Compare commits
879 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da9540a97 | ||
|
|
44da286d5d | ||
|
|
96b6b81d78 | ||
|
|
de4f65e69b | ||
|
|
bce3b48ac7 | ||
|
|
2c1eb27f79 | ||
|
|
a423b15d53 | ||
|
|
162fcf4c2d | ||
|
|
2613fdcc6b | ||
|
|
fbabd509f0 | ||
|
|
a075c93e6f | ||
|
|
d2a8bf92d9 | ||
|
|
39459fb5a1 | ||
|
|
c11f4b9064 | ||
|
|
4fb47e1c3b | ||
|
|
1260a6480f | ||
|
|
598c6ebb69 | ||
|
|
289e2d80c1 | ||
|
|
cb7cd9aac2 | ||
|
|
99fca11958 | ||
|
|
ea020aa8a5 | ||
|
|
4d30fecc36 | ||
|
|
369bbbe084 | ||
|
|
257a4a458b | ||
|
|
0ddc3cc10b | ||
|
|
9c6f1daa83 | ||
|
|
cb9701eaef | ||
|
|
afc2fe2f79 | ||
|
|
0dd00645f8 | ||
|
|
c7919e3c75 | ||
|
|
fb812554f3 | ||
|
|
63094494d3 | ||
|
|
5ff73256e8 | ||
|
|
06d861c16c | ||
|
|
cfe1a40491 | ||
|
|
01fe047f2b | ||
|
|
a4dc0fb0e6 | ||
|
|
0905ad98c1 | ||
|
|
6a309a9624 | ||
|
|
7969e51231 | ||
|
|
933d6c7d0e | ||
|
|
3c879b2523 | ||
|
|
783d016bd1 | ||
|
|
1436122b01 | ||
|
|
922e4f993b | ||
|
|
16d79ec11a | ||
|
|
f08158342a | ||
|
|
be7437d117 | ||
|
|
1a76eb8a83 | ||
|
|
24851106bd | ||
|
|
46e3692a03 | ||
|
|
196fbab5b1 | ||
|
|
f9e6e75c0c | ||
|
|
61363a1985 | ||
|
|
2c639c8f4e | ||
|
|
3cdf77ce54 | ||
|
|
1719e5dd21 | ||
|
|
2436aa527c | ||
|
|
855b2b5c36 | ||
|
|
b78b86cea3 | ||
|
|
047eec686d | ||
|
|
ff28962660 | ||
|
|
6751ebd110 | ||
|
|
03fbe79152 | ||
|
|
9abba8fd01 | ||
|
|
787e4cef0b | ||
|
|
56de3e28f5 | ||
|
|
4c1aeaab3d | ||
|
|
f9366f6da6 | ||
|
|
a9f3624a9b | ||
|
|
3305692808 | ||
|
|
4c50336793 | ||
|
|
ed89ea4a78 | ||
|
|
58bde8c26a | ||
|
|
39d4e94fbb | ||
|
|
a87e394b6d | ||
|
|
b48d11374f | ||
|
|
a8f913c020 | ||
|
|
3f4877a3a0 | ||
|
|
5feb6ffe34 | ||
|
|
a5adcfec97 | ||
|
|
90b61d035e | ||
|
|
05c21fb0a8 | ||
|
|
28cdf8e649 | ||
|
|
d1f2781929 | ||
|
|
6b57da0d8a | ||
|
|
b38f94ef39 | ||
|
|
06e3c76ac6 | ||
|
|
885bf041c4 | ||
|
|
ffe8574c51 | ||
|
|
910d34c721 | ||
|
|
5fec3a97c5 | ||
|
|
37f7dddac7 | ||
|
|
05d912e658 | ||
|
|
4bd87b2953 | ||
|
|
a4c843e51e | ||
|
|
7f9295ad54 | ||
|
|
0ab3d16be9 | ||
|
|
93f2e18811 | ||
|
|
ccd7324590 | ||
|
|
7323e8aa08 | ||
|
|
035830d7bc | ||
|
|
eaaefe2cf5 | ||
|
|
9499caf7b8 | ||
|
|
a677cf87cd | ||
|
|
2fc872ae6f | ||
|
|
27d4d14303 | ||
|
|
434f8819a2 | ||
|
|
97ad2dc158 | ||
|
|
8c1b8b5fda | ||
|
|
e179b7ae66 | ||
|
|
3af18b4fcb | ||
|
|
f4ddff64e9 | ||
|
|
0f587c4292 | ||
|
|
cc7f284d93 | ||
|
|
ebe6c50e15 | ||
|
|
19a4671e59 | ||
|
|
e505afe9df | ||
|
|
2632c6ed86 | ||
|
|
2f5ad3a338 | ||
|
|
7ea45a5f9b | ||
|
|
9d06ffe83a | ||
|
|
11394eaa9f | ||
|
|
8353161451 | ||
|
|
cbc52b977f | ||
|
|
e0b47d7501 | ||
|
|
d92152a557 | ||
|
|
2cc243ac13 | ||
|
|
e735a68102 | ||
|
|
d50ee2217e | ||
|
|
e78d948f82 | ||
|
|
dd10991f93 | ||
|
|
99963c9b24 | ||
|
|
9acac36b85 | ||
|
|
460f10b4ad | ||
|
|
48f5e7c4ab | ||
|
|
866504c2ab | ||
|
|
ec6417feed | ||
|
|
b02f65888c | ||
|
|
9db97d0730 | ||
|
|
36a35279f2 | ||
|
|
d9f3c682d0 | ||
|
|
42dee6cfa9 | ||
|
|
d41e73727a | ||
|
|
9b2755020d | ||
|
|
91388a787a | ||
|
|
2690ab324f | ||
|
|
84993f7bd6 | ||
|
|
5b5cac925c | ||
|
|
854ad45d84 | ||
|
|
7078943b90 | ||
|
|
d59a4799f5 | ||
|
|
cac9d4cc71 | ||
|
|
8aff900d4a | ||
|
|
3c99593599 | ||
|
|
d14bc739c8 | ||
|
|
6060fc7a69 | ||
|
|
d79163fcb5 | ||
|
|
145cae912b | ||
|
|
058eac160f | ||
|
|
f38cac8d8a | ||
|
|
2e19a423fc | ||
|
|
52e3e25741 | ||
|
|
1201ecbfd3 | ||
|
|
75de6316d2 | ||
|
|
4dbef70bd2 | ||
|
|
a0b5fb1107 | ||
|
|
14800f1f6c | ||
|
|
78231becd9 | ||
|
|
da180e1e20 | ||
|
|
c48be5e980 | ||
|
|
98a71d3af6 | ||
|
|
111693ce9e | ||
|
|
d926520b29 | ||
|
|
1a6e359d02 | ||
|
|
46d7ce0a65 | ||
|
|
fd63bfd5fa | ||
|
|
0778d7e5f6 | ||
|
|
9cb53bbf43 | ||
|
|
a98e37918b | ||
|
|
1d18514ab5 | ||
|
|
75d6a16298 | ||
|
|
d0ce7cb5c5 | ||
|
|
fa0d068c30 | ||
|
|
33d5199f51 | ||
|
|
ac14ce71be | ||
|
|
f4bb6e521e | ||
|
|
5f8aa862b4 | ||
|
|
3621f4cc15 | ||
|
|
b6e36b2605 | ||
|
|
7b55570cbb | ||
|
|
308997523c | ||
|
|
90433d77c6 | ||
|
|
c4bc3bf0e7 | ||
|
|
e9638bee06 | ||
|
|
850662902b | ||
|
|
26867873e2 | ||
|
|
7f491c2403 | ||
|
|
776ca22e77 | ||
|
|
dec31b5a1c | ||
|
|
96dbe526cf | ||
|
|
3113e7309e | ||
|
|
1809c6c195 | ||
|
|
adb84ef7da | ||
|
|
824697490f | ||
|
|
62e01cb9d6 | ||
|
|
87c2f82b80 | ||
|
|
f887f8baa4 | ||
|
|
4d00d97be9 | ||
|
|
b0e946ee29 | ||
|
|
1220b9f47b | ||
|
|
12d2d0ffb0 | ||
|
|
4581113741 | ||
|
|
a20c769eae | ||
|
|
3259316cf1 | ||
|
|
9cc06234e4 | ||
|
|
1ee73268d7 | ||
|
|
f477620899 | ||
|
|
8a9dc1ce2c | ||
|
|
b584aa24bd | ||
|
|
8dfe78a210 | ||
|
|
7143cb5def | ||
|
|
e6c49d99b6 | ||
|
|
6e71f9771c | ||
|
|
452126b56a | ||
|
|
28dfd8b3b9 | ||
|
|
d3196bc03f | ||
|
|
0bff3231cd | ||
|
|
6e9d147dd0 | ||
|
|
9a7872a7c4 | ||
|
|
e8f10fb2db | ||
|
|
221f3fcbfd | ||
|
|
ab5153363f | ||
|
|
8576145d3f | ||
|
|
7908fb7d97 | ||
|
|
44b934540e | ||
|
|
9978595237 | ||
|
|
109c701e4f | ||
|
|
11f35d86ff | ||
|
|
fa3005f2b7 | ||
|
|
f30255d6d8 | ||
|
|
183df75112 | ||
|
|
105d06c8f8 | ||
|
|
82941906ca | ||
|
|
470fbb1ff3 | ||
|
|
8c6234879e | ||
|
|
689252c715 | ||
|
|
19cf00227b | ||
|
|
c9795dc560 | ||
|
|
188c8f31b2 | ||
|
|
76b2c067f6 | ||
|
|
01316ceac1 | ||
|
|
52f14c5f06 | ||
|
|
6782730d80 | ||
|
|
4235871a00 | ||
|
|
cb27a22fc4 | ||
|
|
e639d4eee3 | ||
|
|
d95b70f1c2 | ||
|
|
fb97553f7c | ||
|
|
b3b3bec6b0 | ||
|
|
baab7a49f6 | ||
|
|
6843fe700e | ||
|
|
0c193de70e | ||
|
|
e42ce414de | ||
|
|
6d347ada98 | ||
|
|
3638241513 | ||
|
|
1c30743a11 | ||
|
|
48d363031b | ||
|
|
d083719b9c | ||
|
|
7fd59b27f4 | ||
|
|
6f4b216bb0 | ||
|
|
5fa82a3f27 | ||
|
|
02d1e30b08 | ||
|
|
1e2d227c56 | ||
|
|
cfc895e658 | ||
|
|
3f789b39c4 | ||
|
|
16002576d2 | ||
|
|
7b0e352d29 | ||
|
|
da85729254 | ||
|
|
6a48f6c42b | ||
|
|
8eabcd9df3 | ||
|
|
6b89bc9f55 | ||
|
|
1e250796ca | ||
|
|
c73f9028f0 | ||
|
|
71c89371a9 | ||
|
|
9043ddca71 | ||
|
|
6c461e778d | ||
|
|
3f1a71c643 | ||
|
|
b0df6dcade | ||
|
|
ac0d2fb536 | ||
|
|
48f107021a | ||
|
|
64ac4ecf72 | ||
|
|
ddc680a9ae | ||
|
|
43b04da5a2 | ||
|
|
bc28dd392d | ||
|
|
0d2e1fee43 | ||
|
|
eaf2cec8c9 | ||
|
|
611d3dfd4d | ||
|
|
c4030d8267 | ||
|
|
630752787a | ||
|
|
c07ecc58cb | ||
|
|
89fccfe7b7 | ||
|
|
ceeb7e538b | ||
|
|
8dab57b59c | ||
|
|
7a00cd8db1 | ||
|
|
daa0915bca | ||
|
|
ca6b6a30d8 | ||
|
|
09f2640879 | ||
|
|
f61c55b350 | ||
|
|
78d0c2540c | ||
|
|
1d0426e816 | ||
|
|
103a4049c8 | ||
|
|
cbcb1dcb33 | ||
|
|
9c36293382 | ||
|
|
2fb36dc425 | ||
|
|
c717ba5a71 | ||
|
|
7572518e3b | ||
|
|
f670133a82 | ||
|
|
a6bbf41e82 | ||
|
|
622c16932a | ||
|
|
5fd7e0ed22 | ||
|
|
d9f6c71015 | ||
|
|
61a3106b3b | ||
|
|
20eb2d825d | ||
|
|
906557d2ec | ||
|
|
c1e278ea80 | ||
|
|
e2d616a53f | ||
|
|
c6802ccdd2 | ||
|
|
abd50fd037 | ||
|
|
4515be53b4 | ||
|
|
920a25bb25 | ||
|
|
648dd05069 | ||
|
|
713edcecca | ||
|
|
ac16a951c5 | ||
|
|
1043bcb267 | ||
|
|
22979a1a77 | ||
|
|
3039d2e7eb | ||
|
|
557dee2d8c | ||
|
|
a506adcb64 | ||
|
|
3bfb120646 | ||
|
|
43e613aa52 | ||
|
|
0d930ed605 | ||
|
|
2014f1e4b3 | ||
|
|
4889d17e0a | ||
|
|
494d31215d | ||
|
|
582bdea431 | ||
|
|
ecf7df9c01 | ||
|
|
a9760e8817 | ||
|
|
b32e990dd4 | ||
|
|
4320d2436f | ||
|
|
cba6e74b13 | ||
|
|
981bcf5fa2 | ||
|
|
1d79bc37d3 | ||
|
|
2fae3336ba | ||
|
|
cfb98c5139 | ||
|
|
b0260327c4 | ||
|
|
f65532e347 | ||
|
|
ff574b046c | ||
|
|
97c995b64c | ||
|
|
8361ec97e4 | ||
|
|
7a0bbc0acb | ||
|
|
66f5eca841 | ||
|
|
b53e6d1888 | ||
|
|
4b68fa8b60 | ||
|
|
54770c5a50 | ||
|
|
39fb2167f7 | ||
|
|
c33fef9c98 | ||
|
|
08a1117edf | ||
|
|
e14cbed56e | ||
|
|
56e2ab16cc | ||
|
|
d76d4b70f5 | ||
|
|
e223936a81 | ||
|
|
60d067c421 | ||
|
|
4102cf4688 | ||
|
|
dc977e6630 | ||
|
|
05deabd775 | ||
|
|
549ab4aa15 | ||
|
|
b189c71682 | ||
|
|
b909856933 | ||
|
|
5de9084901 | ||
|
|
384ef0b44c | ||
|
|
9307772dc3 | ||
|
|
730c71d103 | ||
|
|
5c710b96f5 | ||
|
|
fe63d0eadf | ||
|
|
a6ca95159a | ||
|
|
677d32fef5 | ||
|
|
14abd05969 | ||
|
|
2e680be34f | ||
|
|
fe29942bf4 | ||
|
|
c8fb717ac1 | ||
|
|
1ff7fcc913 | ||
|
|
419c32702a | ||
|
|
9b63714caa | ||
|
|
f034233607 | ||
|
|
be6fcb51b6 | ||
|
|
e49a682f00 | ||
|
|
23ad3141a1 | ||
|
|
5347e374e0 | ||
|
|
1a49a628de | ||
|
|
8def256d7e | ||
|
|
1cd8e6bad7 | ||
|
|
7a03c0db25 | ||
|
|
e7e3657d1f | ||
|
|
734dfcc9bc | ||
|
|
b0db15099d | ||
|
|
6fbd2369ba | ||
|
|
f4a6674eed | ||
|
|
c0567ad4f5 | ||
|
|
f146ee7e9f | ||
|
|
e606f4ce18 | ||
|
|
945b589a58 | ||
|
|
b18042c4a8 | ||
|
|
a9ff39104b | ||
|
|
f6af6e5880 | ||
|
|
57c6c2d471 | ||
|
|
c362527903 | ||
|
|
a7acbd0738 | ||
|
|
f67192ebce | ||
|
|
c44fde83e4 | ||
|
|
50119285ef | ||
|
|
6216916fed | ||
|
|
2952380200 | ||
|
|
fb42b82e0d | ||
|
|
6d381ab88d | ||
|
|
c5c1de32bc | ||
|
|
8077ad9bcd | ||
|
|
6f22ba350f | ||
|
|
f23d0c0157 | ||
|
|
a9a38edf24 | ||
|
|
a5534f1e49 | ||
|
|
1c6469f384 | ||
|
|
8cfdbc1196 | ||
|
|
88737ca6ea | ||
|
|
45bebc60bd | ||
|
|
4f7dec4635 | ||
|
|
98739cce5a | ||
|
|
0bfbfacc27 | ||
|
|
73cd862e83 | ||
|
|
3305e9b74f | ||
|
|
c37ec0e8d0 | ||
|
|
0b005477c1 | ||
|
|
a1467f8dac | ||
|
|
40d2c34347 | ||
|
|
528270e767 | ||
|
|
f4a04b2387 | ||
|
|
14ed6ae109 | ||
|
|
4fd43694ae | ||
|
|
552d731e6a | ||
|
|
49ac3ef528 | ||
|
|
4379313f12 | ||
|
|
3901949f36 | ||
|
|
a3d8593fed | ||
|
|
7c5baeb9c7 | ||
|
|
c692db5f85 | ||
|
|
9130196ffc | ||
|
|
dad4a65118 | ||
|
|
4c34c2feb7 | ||
|
|
23522f7775 | ||
|
|
82c61398ba | ||
|
|
02871050a6 | ||
|
|
7d3b1fea6b | ||
|
|
24917fa2a6 | ||
|
|
de3d8e4a23 | ||
|
|
1502845d65 | ||
|
|
af3f10f74e | ||
|
|
c100372b31 | ||
|
|
72d699b39a | ||
|
|
7d2b8a2a8b | ||
|
|
8729c68e22 | ||
|
|
e2d48bedd9 | ||
|
|
6b241ce9b3 | ||
|
|
1b68bdb36c | ||
|
|
fb82538441 | ||
|
|
2709d0869a | ||
|
|
343f87bbe7 | ||
|
|
ecbfc6004c | ||
|
|
c57eecc81b | ||
|
|
7ea14dc03f | ||
|
|
4340d27258 | ||
|
|
e911be8f14 | ||
|
|
a4c650cdff | ||
|
|
31a58f8a8f | ||
|
|
ba4f15f111 | ||
|
|
206a913eb9 | ||
|
|
21ba490073 | ||
|
|
2a29e2ed95 | ||
|
|
9517d14fd3 | ||
|
|
3deaa896df | ||
|
|
c117007dc0 | ||
|
|
50baa238b9 | ||
|
|
0e2d771660 | ||
|
|
32b4670755 | ||
|
|
4a032d5e12 | ||
|
|
5887fede15 | ||
|
|
ad4eeb9f81 | ||
|
|
a62c16d7cc | ||
|
|
e766e7392a | ||
|
|
025c9c24ca | ||
|
|
ab052add27 | ||
|
|
15cb99977b | ||
|
|
82d5af926f | ||
|
|
76f73f3dc8 | ||
|
|
575b22320e | ||
|
|
d20c07dc85 | ||
|
|
f89071b87a | ||
|
|
8b4e90f285 | ||
|
|
9c4f57c786 | ||
|
|
902ec24b77 | ||
|
|
7eba77fa63 | ||
|
|
0753bbf7b3 | ||
|
|
6b2333614a | ||
|
|
80b7f458f5 | ||
|
|
dbd8e59cf4 | ||
|
|
9948230ea0 | ||
|
|
e2c858ac69 | ||
|
|
bac8b613e6 | ||
|
|
abbae15c6f | ||
|
|
1548e8bfc1 | ||
|
|
dc8cf3fc34 | ||
|
|
c3cd815567 | ||
|
|
ce543b9384 | ||
|
|
9755c8cf42 | ||
|
|
434a210fb5 | ||
|
|
587602665a | ||
|
|
bfec63df41 | ||
|
|
3b150df1af | ||
|
|
f24ecf0537 | ||
|
|
9ddecbcc0a | ||
|
|
947bb8d3d5 | ||
|
|
30cbf87b35 | ||
|
|
69c3aab35a | ||
|
|
bdeb3547f1 | ||
|
|
99e04b9669 | ||
|
|
680d2fb7eb | ||
|
|
8814eda018 | ||
|
|
7e72ba2885 | ||
|
|
b4d7a8490b | ||
|
|
e9a5bc66df | ||
|
|
edebc77726 | ||
|
|
271dee824d | ||
|
|
1e868d10ca | ||
|
|
4be3f17ae4 | ||
|
|
f24e7be264 | ||
|
|
9adc26445d | ||
|
|
353e7dcbb9 | ||
|
|
430e65c12e | ||
|
|
1aa242a9d8 | ||
|
|
7173cd85fe | ||
|
|
b95a4f55e3 | ||
|
|
6b5e016770 | ||
|
|
85aa9f61cd | ||
|
|
5c187002d6 | ||
|
|
9bc373308b | ||
|
|
cdeb0fc144 | ||
|
|
f1acdd9389 | ||
|
|
d6fac6a210 | ||
|
|
007cfb0801 | ||
|
|
1f9829b7c0 | ||
|
|
e039d22565 | ||
|
|
b0775b1610 | ||
|
|
0e429700c6 | ||
|
|
af7804ca23 | ||
|
|
9da326967b | ||
|
|
62600b3a66 | ||
|
|
b236138fb5 | ||
|
|
40c2e9a54b | ||
|
|
a9062db57f | ||
|
|
2621404c5f | ||
|
|
c47211ca79 | ||
|
|
e39a192e8d | ||
|
|
d85035d5ef | ||
|
|
de09a97343 | ||
|
|
a6855345d7 | ||
|
|
a53f88b626 | ||
|
|
7048c82124 | ||
|
|
6aa7cda478 | ||
|
|
ff339b9a8c | ||
|
|
8898cc20fe | ||
|
|
770d1da280 | ||
|
|
6ba4e8a29b | ||
|
|
953ca68495 | ||
|
|
4289dfb37d | ||
|
|
4f6f2f436a | ||
|
|
237979a479 | ||
|
|
2e48968fd3 | ||
|
|
9a0c4a5c8f | ||
|
|
9a011f0007 | ||
|
|
3f907a706f | ||
|
|
9446141716 | ||
|
|
1994826af8 | ||
|
|
ab950d6ffc | ||
|
|
b77e611a90 | ||
|
|
86f0284894 | ||
|
|
9bbe014dfe | ||
|
|
ad92c95500 | ||
|
|
d360190382 | ||
|
|
1737c8a7f6 | ||
|
|
cde262fd66 | ||
|
|
bd74689079 | ||
|
|
248942bdea | ||
|
|
d9f203300b | ||
|
|
aceabc969f | ||
|
|
dedc24d3a7 | ||
|
|
6e583e78e8 | ||
|
|
c012e83355 | ||
|
|
264355d185 | ||
|
|
fdbfd1ec60 | ||
|
|
7a8b27a255 | ||
|
|
ec4bfac98b | ||
|
|
c63ffe37c9 | ||
|
|
d2f3ce82c9 | ||
|
|
3e24a0b0a4 | ||
|
|
1a07e29ff4 | ||
|
|
1aa46a8928 | ||
|
|
d9083f8b5f | ||
|
|
23d558a6d7 | ||
|
|
665a5b7b12 | ||
|
|
1d73418969 | ||
|
|
f67b5e4cc4 | ||
|
|
ae2515444f | ||
|
|
463e77f0a5 | ||
|
|
d7b796b1a7 | ||
|
|
9b07d53077 | ||
|
|
8ee9b2bc31 | ||
|
|
c5989477a4 | ||
|
|
96d8a4e4f8 | ||
|
|
e865e11731 | ||
|
|
f0997bfe0d | ||
|
|
8b67dad456 | ||
|
|
53fdac1038 | ||
|
|
534a7602e6 | ||
|
|
30f329fe43 | ||
|
|
4ce39951a9 | ||
|
|
0e9eb34626 | ||
|
|
0ff299c425 | ||
|
|
6366258ce9 | ||
|
|
bca69a026e | ||
|
|
adc26ea42a | ||
|
|
84422b10c8 | ||
|
|
d05ad0f8f4 | ||
|
|
3f70ddaffa | ||
|
|
b16e8d84d7 | ||
|
|
5ee405d5a0 | ||
|
|
a5b9470636 | ||
|
|
16a3d54cb6 | ||
|
|
9677dcd978 | ||
|
|
17ea42f918 | ||
|
|
bd8915918d | ||
|
|
91db6773a0 | ||
|
|
f50680b61f | ||
|
|
3fb5d33333 | ||
|
|
f70bbb2660 | ||
|
|
589c1ac9bb | ||
|
|
d458fa1b89 | ||
|
|
dc2c99434f | ||
|
|
bbf36562d0 | ||
|
|
c458edf3e4 | ||
|
|
99c2aaef7a | ||
|
|
921096ca10 | ||
|
|
b0fbd6fa36 | ||
|
|
21bd67c44f | ||
|
|
4f142985a2 | ||
|
|
bfa89b44bc | ||
|
|
eba876e72b | ||
|
|
f1a7413e20 | ||
|
|
4d32749489 | ||
|
|
d911a16684 | ||
|
|
d7193bc755 | ||
|
|
aee9ffdb9c | ||
|
|
1f627d5e49 | ||
|
|
bb67626d09 | ||
|
|
4e0153e94a | ||
|
|
065142ff19 | ||
|
|
8bb6fae52f | ||
|
|
8cb7b81903 | ||
|
|
cb214ccda9 | ||
|
|
3fa7b96f27 | ||
|
|
7c8e259072 | ||
|
|
743d067042 | ||
|
|
50ea410718 | ||
|
|
e71182aed2 | ||
|
|
97f7963e0b | ||
|
|
6cea6a10bd | ||
|
|
344d41e365 | ||
|
|
597a8adfed | ||
|
|
5682e5483a | ||
|
|
18779edd7d | ||
|
|
63aeaea77a | ||
|
|
f51e48cb40 | ||
|
|
193372c79c | ||
|
|
34d0afe7e5 | ||
|
|
d33a9dd56d | ||
|
|
af2be90176 | ||
|
|
701fb21544 | ||
|
|
c8f010d9a6 | ||
|
|
c1ca7580e2 | ||
|
|
11d3c2cbf1 | ||
|
|
412f576aee | ||
|
|
ff98a7b23b | ||
|
|
507bac8542 | ||
|
|
3bd6cf4805 | ||
|
|
5712b31869 | ||
|
|
b0f9a0b18f | ||
|
|
f7665b460e | ||
|
|
100029b640 | ||
|
|
2b89bddf0f | ||
|
|
ca46bad8ec | ||
|
|
1ecd2600a3 | ||
|
|
28306b126b | ||
|
|
5c842e192b | ||
|
|
f9f307118d | ||
|
|
2266fa4f4b | ||
|
|
6df2564d1a | ||
|
|
b2a62ebd26 | ||
|
|
2fca30752a | ||
|
|
210eced369 | ||
|
|
08c574aff8 | ||
|
|
f4db2d65f1 | ||
|
|
23b02ade96 | ||
|
|
71d35b16ac | ||
|
|
200058c52a | ||
|
|
edb5fb202a | ||
|
|
d114c25cdb | ||
|
|
64e469568b | ||
|
|
c63d777372 | ||
|
|
ae343b8cb0 | ||
|
|
86eb5d2d57 | ||
|
|
b562d241db | ||
|
|
49b1829b78 | ||
|
|
1a5bf4035c | ||
|
|
f3810a1504 | ||
|
|
4831760c11 | ||
|
|
f375d8991e | ||
|
|
8145a4a3a6 | ||
|
|
12c9b49d4c | ||
|
|
c8eb81161e | ||
|
|
8ab246a66c | ||
|
|
8dfc4797fa | ||
|
|
7888fe1176 | ||
|
|
8a6f3056a3 | ||
|
|
5fbdf9e013 | ||
|
|
1da2f5e823 | ||
|
|
888779f81b | ||
|
|
352286e497 | ||
|
|
025ce37c05 | ||
|
|
d6c2c63679 | ||
|
|
49efbf25ea | ||
|
|
02cdee2776 | ||
|
|
7c9d4d5b05 | ||
|
|
609b7551f8 | ||
|
|
e8f215ae00 | ||
|
|
2e00aea16e | ||
|
|
dd274d77f5 | ||
|
|
58171a7b8c | ||
|
|
8b05179401 | ||
|
|
51d61a7e88 | ||
|
|
d653ce4e0e | ||
|
|
07b3826806 | ||
|
|
1baa3e23b2 | ||
|
|
0aa1f20d47 | ||
|
|
1cf330b389 | ||
|
|
1771c4b346 | ||
|
|
4c053b4873 | ||
|
|
743ba0541b | ||
|
|
cfab2a9cd7 | ||
|
|
32270efd65 | ||
|
|
7ea1acb7c1 | ||
|
|
bf91f60242 | ||
|
|
660885c0b1 | ||
|
|
15fd3b969f | ||
|
|
f1d15ca7f2 | ||
|
|
6f4f4a5924 | ||
|
|
9af0520701 | ||
|
|
2edeab558e | ||
|
|
87bf59f50b | ||
|
|
eeb69e63f7 | ||
|
|
f9435906e7 | ||
|
|
6c8adbe50e | ||
|
|
23bdb6c579 | ||
|
|
264411bfb9 | ||
|
|
2104237584 | ||
|
|
0ae2525737 | ||
|
|
b12973a837 | ||
|
|
fa0582ce0b | ||
|
|
231f5157bf | ||
|
|
8b18204a69 | ||
|
|
95eb6a732c | ||
|
|
047a188b34 | ||
|
|
d407815c30 | ||
|
|
1f0f87633b | ||
|
|
c15ff4e32e | ||
|
|
72bddca314 | ||
|
|
496fc4ebee | ||
|
|
f414e6eeb7 | ||
|
|
f09606cfa3 | ||
|
|
6304fe4c19 | ||
|
|
5f2b8f8a2e | ||
|
|
898e8d4546 | ||
|
|
f1657164d5 | ||
|
|
357e13be2b | ||
|
|
9685568c75 | ||
|
|
b316940790 | ||
|
|
2ced489e1e | ||
|
|
5969fe08d8 | ||
|
|
4a427f1ff6 | ||
|
|
9a3db275f3 | ||
|
|
475dd4d1ff | ||
|
|
57c99c4a34 | ||
|
|
966f5691a2 | ||
|
|
5088ece8a1 | ||
|
|
943d87fe17 | ||
|
|
b5363b2689 | ||
|
|
c15cb16ca8 | ||
|
|
18b7f088fc | ||
|
|
4f9822743c | ||
|
|
e7925de5bc | ||
|
|
27fc6a7279 | ||
|
|
ab5f46e955 | ||
|
|
d30d212cc5 | ||
|
|
adff971d62 | ||
|
|
23b22f71b8 | ||
|
|
fee3671e32 | ||
|
|
26c6be7268 | ||
|
|
01c5bcf2be | ||
|
|
1ab8a5ab98 | ||
|
|
b54aaca28a | ||
|
|
86a29ae000 | ||
|
|
a5dbee93ff | ||
|
|
e0465e6e10 | ||
|
|
7da48b9dd1 | ||
|
|
a64895c3a6 | ||
|
|
21f1a5d4c4 | ||
|
|
d60f79ca33 | ||
|
|
2d5cea5033 | ||
|
|
b0615215fe | ||
|
|
7a0f98b2cb | ||
|
|
cdc1dab4a6 | ||
|
|
431739ab19 | ||
|
|
1780399050 | ||
|
|
eb75d38716 | ||
|
|
06f82d78f4 | ||
|
|
dee4da1c0e | ||
|
|
e341ec7c60 | ||
|
|
9146609e4a | ||
|
|
f90615ca41 | ||
|
|
8a2a6a05ff | ||
|
|
5a787f8ed5 | ||
|
|
a904053002 | ||
|
|
70ecd30dcc | ||
|
|
b0976d7e47 | ||
|
|
bb5e55016c | ||
|
|
abdf285c69 | ||
|
|
fd4da23d4f | ||
|
|
bb8f58c83b | ||
|
|
077da6ae86 | ||
|
|
d5222d7e9a | ||
|
|
01702c127b | ||
|
|
87902cbfb4 | ||
|
|
fcaa393ffe | ||
|
|
18bac6e792 | ||
|
|
9a21efebe3 | ||
|
|
357182ef17 | ||
|
|
5fad540a4c | ||
|
|
c1fc153420 | ||
|
|
73a1974f85 | ||
|
|
dec73c21b6 | ||
|
|
46a00cc864 | ||
|
|
62455be165 | ||
|
|
17ef5ef918 | ||
|
|
922b6fffd0 | ||
|
|
b47bf006d0 | ||
|
|
d60269f4bc | ||
|
|
1ce057a78e | ||
|
|
a0fe04b990 | ||
|
|
31c9172e19 | ||
|
|
7f23cbef71 | ||
|
|
4884defaed | ||
|
|
3039218c40 |
7
.codeclimate.yml
Normal file
7
.codeclimate.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: "2"
|
||||||
|
# https://docs.codeclimate.com/docs/default-analysis-configuration#sample-codeclimateyml
|
||||||
|
checks:
|
||||||
|
method-complexity:
|
||||||
|
enabled: true
|
||||||
|
config:
|
||||||
|
threshold: 15
|
||||||
@@ -12,5 +12,6 @@ spec/
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
**/*.orig
|
**/*.orig
|
||||||
*.orig
|
*.orig
|
||||||
bin/wpscan-docker*
|
bin/wpscan-*
|
||||||
.wpscan/
|
.wpscan/
|
||||||
|
.github/
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Before submitting an issue, please make sure you fully read any potential error messages output and did some research on your own.
|
||||||
|
|
||||||
### Subject of the issue
|
### Subject of the issue
|
||||||
Describe your issue here.
|
Describe your issue here.
|
||||||
|
|
||||||
@@ -24,4 +35,4 @@ Things you have tried (where relevant):
|
|||||||
* Update Ruby to the latest version [ ]
|
* Update Ruby to the latest version [ ]
|
||||||
* Ensure you can reach the target site using cURL [ ]
|
* Ensure you can reach the target site using cURL [ ]
|
||||||
* Proxied WPScan through a HTTP proxy to view the raw traffic [ ]
|
* Proxied WPScan through a HTTP proxy to view the raw traffic [ ]
|
||||||
* Ensure you are using a supported Operating System (Linux and macOS) [ ]
|
* Ensure you are using a supported Operating System (Linux and macOS) [ ]
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
10
.github/ISSUE_TEMPLATE/other-issue.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/other-issue.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: Other Issue
|
||||||
|
about: Create a report which is not a related to a Bug or Feature
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Before submitting an issue, please make sure you fully read any potential error messages output and did some research on your own.
|
||||||
17
.github/dependabot.yml
vendored
Normal file
17
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "bundler"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
# Check for updates to GitHub Actions every weekday
|
||||||
|
interval: "daily"
|
||||||
42
.github/workflows/build.yml
vendored
Normal file
42
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
ruby: [2.7, '3.0', 3.1, 3.2]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Ruby ${{ matrix.ruby }}
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: ${{ matrix.ruby }}
|
||||||
|
|
||||||
|
- name: Install GEMs
|
||||||
|
run: |
|
||||||
|
gem install bundler
|
||||||
|
bundle config force_ruby_platform true
|
||||||
|
bundle config path vendor/bundle
|
||||||
|
bundle install --jobs 4 --retry 3
|
||||||
|
|
||||||
|
- name: rubocop
|
||||||
|
run: |
|
||||||
|
bundle exec rubocop
|
||||||
|
|
||||||
|
- name: rspec
|
||||||
|
run: |
|
||||||
|
bundle exec rspec
|
||||||
|
|
||||||
|
- name: Coveralls
|
||||||
|
uses: coverallsapp/github-action@master
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
56
.github/workflows/docker.yml
vendored
Normal file
56
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Build Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
schedule:
|
||||||
|
- cron: "0 7 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
images:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set tag to latest
|
||||||
|
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'schedule'
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set tag to release name
|
||||||
|
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_TAG=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check if DOCKER_TAG is set
|
||||||
|
if: env.DOCKER_TAG == ''
|
||||||
|
run: |
|
||||||
|
echo DOCKER_TAG is not set!
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
id: buildx
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2.2.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: wpscanteam/wpscan:${{ env.DOCKER_TAG }}
|
||||||
40
.github/workflows/gempush.yml
vendored
Normal file
40
.github/workflows/gempush.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Ruby Gem
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build + Publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Ruby 2.6
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 2.6
|
||||||
|
|
||||||
|
#- name: Publish to GPR
|
||||||
|
# run: |
|
||||||
|
# mkdir -p $HOME/.gem
|
||||||
|
# touch $HOME/.gem/credentials
|
||||||
|
# chmod 0600 $HOME/.gem/credentials
|
||||||
|
# printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
||||||
|
# gem build *.gemspec
|
||||||
|
# gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
||||||
|
# env:
|
||||||
|
# GEM_HOST_API_KEY: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
# OWNER: wpscanteam
|
||||||
|
|
||||||
|
- name: Publish to RubyGems
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/.gem
|
||||||
|
touch $HOME/.gem/credentials
|
||||||
|
chmod 0600 $HOME/.gem/credentials
|
||||||
|
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
||||||
|
gem build *.gemspec
|
||||||
|
gem push *.gem
|
||||||
|
env:
|
||||||
|
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ doc/
|
|||||||
# Old files from v2
|
# Old files from v2
|
||||||
cache/
|
cache/
|
||||||
data/
|
data/
|
||||||
|
|
||||||
|
# Profiling reports
|
||||||
|
bin/memprof*.report
|
||||||
|
|||||||
34
.rubocop.yml
34
.rubocop.yml
@@ -1,28 +1,42 @@
|
|||||||
|
require: rubocop-performance
|
||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 2.3
|
NewCops: enable
|
||||||
|
SuggestExtensions: false
|
||||||
|
TargetRubyVersion: 2.7
|
||||||
Exclude:
|
Exclude:
|
||||||
- '*.gemspec'
|
- '*.gemspec'
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
ClassVars:
|
Layout/LineLength:
|
||||||
Enabled: false
|
|
||||||
LineLength:
|
|
||||||
Max: 120
|
Max: 120
|
||||||
MethodLength:
|
Lint/ConstantDefinitionInBlock:
|
||||||
Max: 20
|
Enabled: false
|
||||||
|
Lint/MissingSuper:
|
||||||
|
Enabled: false
|
||||||
Lint/UriEscapeUnescape:
|
Lint/UriEscapeUnescape:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 25
|
Max: 27
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/**/*'
|
- 'spec/**/*'
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 150
|
Max: 150
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/enumeration/cli_options.rb'
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 8
|
Max: 10
|
||||||
Style/Documentation:
|
Metrics/MethodLength:
|
||||||
|
Max: 20
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/enumeration/cli_options.rb'
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Max: 11
|
||||||
|
Style/ClassVars:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/FrozenStringLiteralComment:
|
Style/Documentation:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/FormatStringToken:
|
Style/FormatStringToken:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/vuln_api.rb'
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.5.3
|
3.0.2
|
||||||
|
|||||||
19
.simplecov
Normal file
19
.simplecov
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
if ENV['GITHUB_ACTION']
|
||||||
|
require 'simplecov-lcov'
|
||||||
|
|
||||||
|
SimpleCov::Formatter::LcovFormatter.config do |c|
|
||||||
|
c.single_report_path = 'coverage/lcov.info'
|
||||||
|
c.report_with_single_file = true
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
|
||||||
|
end
|
||||||
|
|
||||||
|
SimpleCov.start do
|
||||||
|
enable_coverage :branch # Only supported for Ruby >= 2.5
|
||||||
|
|
||||||
|
add_filter '/spec/'
|
||||||
|
add_filter 'helper'
|
||||||
|
end
|
||||||
35
.travis.yml
35
.travis.yml
@@ -1,35 +0,0 @@
|
|||||||
language: ruby
|
|
||||||
sudo: false
|
|
||||||
cache: bundler
|
|
||||||
rvm:
|
|
||||||
- 2.3.0
|
|
||||||
- 2.3.1
|
|
||||||
- 2.3.2
|
|
||||||
- 2.3.3
|
|
||||||
- 2.3.4
|
|
||||||
- 2.3.5
|
|
||||||
- 2.3.6
|
|
||||||
- 2.3.7
|
|
||||||
- 2.3.8
|
|
||||||
- 2.4.1
|
|
||||||
- 2.4.2
|
|
||||||
- 2.4.3
|
|
||||||
- 2.4.4
|
|
||||||
- 2.4.5
|
|
||||||
- 2.5.0
|
|
||||||
- 2.5.1
|
|
||||||
- 2.5.2
|
|
||||||
- 2.5.3
|
|
||||||
- ruby-head
|
|
||||||
before_install:
|
|
||||||
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
|
|
||||||
- "gem update --system"
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- rvm: ruby-head
|
|
||||||
script:
|
|
||||||
- bundle exec rubocop
|
|
||||||
- bundle exec rspec
|
|
||||||
notifications:
|
|
||||||
email:
|
|
||||||
- team@wpscan.org
|
|
||||||
25
Dockerfile
25
Dockerfile
@@ -1,16 +1,16 @@
|
|||||||
FROM ruby:2.5.1-alpine AS builder
|
FROM ruby:3.0.2-alpine AS builder
|
||||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
LABEL maintainer="WPScan Team <contact@wpscan.com>"
|
||||||
|
|
||||||
ARG BUNDLER_ARGS="--jobs=8 --without test development"
|
RUN echo "install: --no-document --no-post-install-message\nupdate: --no-document --no-post-install-message" > /etc/gemrc
|
||||||
|
|
||||||
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
|
|
||||||
|
|
||||||
COPY . /wpscan
|
COPY . /wpscan
|
||||||
|
|
||||||
RUN apk add --no-cache git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \
|
RUN apk add --no-cache git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \
|
||||||
bundle install --system --clean --no-cache --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \
|
bundle config force_ruby_platform true && \
|
||||||
# temp fix for https://github.com/bundler/bundler/issues/6680
|
bundle config disable_version_check 'true' && \
|
||||||
rm -rf /usr/local/bundle/cache
|
bundle config without "test development" && \
|
||||||
|
bundle config path.system 'true' && \
|
||||||
|
bundle install --gemfile=/wpscan/Gemfile --jobs=8
|
||||||
|
|
||||||
WORKDIR /wpscan
|
WORKDIR /wpscan
|
||||||
RUN rake install --trace
|
RUN rake install --trace
|
||||||
@@ -19,20 +19,23 @@ RUN rake install --trace
|
|||||||
RUN chmod -R a+r /usr/local/bundle
|
RUN chmod -R a+r /usr/local/bundle
|
||||||
|
|
||||||
|
|
||||||
FROM ruby:2.5-alpine
|
FROM ruby:3.0.2-alpine
|
||||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
LABEL maintainer="WPScan Team <contact@wpscan.com>"
|
||||||
|
LABEL org.opencontainers.image.source https://github.com/wpscanteam/wpscan
|
||||||
|
|
||||||
RUN adduser -h /wpscan -g WPScan -D wpscan
|
RUN adduser -h /wpscan -g WPScan -D wpscan
|
||||||
|
|
||||||
COPY --from=builder /usr/local/bundle /usr/local/bundle
|
COPY --from=builder /usr/local/bundle /usr/local/bundle
|
||||||
|
|
||||||
RUN chown -R wpscan:wpscan /wpscan
|
RUN chown -R wpscan:wpscan /wpscan
|
||||||
|
|
||||||
# runtime dependencies
|
# runtime dependencies
|
||||||
RUN apk add --no-cache libcurl procps sqlite-libs
|
RUN apk add --no-cache libcurl procps sqlite-libs
|
||||||
|
|
||||||
|
WORKDIR /wpscan
|
||||||
|
|
||||||
USER wpscan
|
USER wpscan
|
||||||
|
|
||||||
RUN /usr/local/bundle/bin/wpscan --update --verbose
|
RUN /usr/local/bundle/bin/wpscan --update --verbose
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
|
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
|
||||||
CMD ["--help"]
|
|
||||||
|
|||||||
4
Gemfile
4
Gemfile
@@ -1,2 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
|
# gem 'cms_scanner', branch: 'xxx', git: 'https://github.com/wpscanteam/CMSScanner.git'
|
||||||
|
|||||||
14
LICENSE
14
LICENSE
@@ -1,14 +1,14 @@
|
|||||||
WPScan Public Source License
|
WPScan Public Source License
|
||||||
|
|
||||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
|
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2019 WPScan Team.
|
||||||
|
|
||||||
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
||||||
|
|
||||||
1. Definitions
|
1. Definitions
|
||||||
|
|
||||||
1.1 “License” means this document.
|
1.1 "License" means this document.
|
||||||
1.2 “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
|
1.2 "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
|
||||||
1.3 “WPScan Team” means WPScan’s core developers, an updated list of whom can be found within the CREDITS file.
|
1.3 "WPScan Team" means WPScan’s core developers.
|
||||||
|
|
||||||
2. Commercialization
|
2. Commercialization
|
||||||
|
|
||||||
@@ -27,9 +27,7 @@ Example cases which do not require a commercial license, and thus fall under the
|
|||||||
- Using WPScan to test your own systems.
|
- Using WPScan to test your own systems.
|
||||||
- Any non-commercial use of WPScan.
|
- Any non-commercial use of WPScan.
|
||||||
|
|
||||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
|
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - contact@wpscan.com.
|
||||||
|
|
||||||
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
|
|
||||||
|
|
||||||
Free-use Terms and Conditions;
|
Free-use Terms and Conditions;
|
||||||
|
|
||||||
@@ -59,7 +57,7 @@ WPScan is provided under an AS-IS basis and without any support, updates or main
|
|||||||
|
|
||||||
8. Disclaimer of Warranty
|
8. Disclaimer of Warranty
|
||||||
|
|
||||||
WPScan is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
|
WPScan is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||||
|
|
||||||
9. Limitation of Liability
|
9. Limitation of Liability
|
||||||
|
|
||||||
|
|||||||
172
README.md
172
README.md
@@ -1,111 +1,167 @@
|
|||||||

|
<p align="center">
|
||||||
|
<a href="https://wpscan.com/">
|
||||||
|
<img src="https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png" alt="WPScan logo">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://badge.fury.io/rb/wpscan)
|
<h3 align="center">WPScan</h3>
|
||||||
[](https://travis-ci.org/wpscanteam/wpscan)
|
|
||||||
[](https://codeclimate.com/github/wpscanteam/wpscan)
|
<p align="center">
|
||||||
[](https://www.patreon.com/wpscan)
|
WordPress Security Scanner
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="https://wpscan.com/" title="homepage" target="_blank">WPScan WordPress Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress security plugin" target="_blank">WordPress Security Plugin</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a>
|
||||||
|
<a href="https://hub.docker.com/r/wpscanteam/wpscan/" target="_blank"><img src="https://img.shields.io/docker/pulls/wpscanteam/wpscan.svg"></a>
|
||||||
|
<a href="https://github.com/wpscanteam/wpscan/actions?query=workflow%3ABuild" target="_blank"><img src="https://github.com/wpscanteam/wpscan/workflows/Build/badge.svg"></a>
|
||||||
|
<a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
# INSTALL
|
# INSTALL
|
||||||
|
|
||||||
## Prerequisites:
|
## Prerequisites
|
||||||
|
|
||||||
- Ruby >= 2.3 - Recommended: latest
|
- (Optional but highly recommended: [RVM](https://rvm.io/rvm/install))
|
||||||
- Curl >= 7.21 - Recommended: latest - FYI the 7.29 has a segfault
|
- Ruby >= 2.7 - Recommended: latest
|
||||||
|
- Curl >= 7.72 - Recommended: latest
|
||||||
|
- The 7.29 has a segfault
|
||||||
|
- The < 7.72 could result in `Stream error in the HTTP/2 framing layer` in some cases
|
||||||
- RubyGems - Recommended: latest
|
- RubyGems - Recommended: latest
|
||||||
|
- Nokogiri might require packages to be installed via your package manager depending on your OS, see https://nokogiri.org/tutorials/installing_nokogiri.html
|
||||||
|
|
||||||
### From RubyGems:
|
### In a Pentesting distribution
|
||||||
|
|
||||||
|
When using a pentesting distubution (such as Kali Linux), it is recommended to install/update wpscan via the package manager if available.
|
||||||
|
|
||||||
|
### In macOSX via Homebrew
|
||||||
|
|
||||||
|
```shell
|
||||||
|
brew install wpscanteam/tap/wpscan
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### From RubyGems
|
||||||
|
|
||||||
|
```shell
|
||||||
gem install wpscan
|
gem install wpscan
|
||||||
```
|
```
|
||||||
|
|
||||||
### From sources:
|
On MacOSX, if a ```Gem::FilePermissionError``` is raised due to the Apple's System Integrity Protection (SIP), either install RVM and install wpscan again, or run ```sudo gem install -n /usr/local/bin wpscan``` (see [#1286](https://github.com/wpscanteam/wpscan/issues/1286))
|
||||||
|
|
||||||
Prerequisites: Git
|
# Updating
|
||||||
|
|
||||||
```
|
You can update the local database by using ```wpscan --update```
|
||||||
git clone https://github.com/wpscanteam/wpscan
|
|
||||||
|
|
||||||
cd wpscan/
|
Updating WPScan itself is either done via ```gem update wpscan``` or the packages manager (this is quite important for distributions such as in Kali Linux: ```apt-get update && apt-get upgrade```) depending on how WPScan was (pre)installed
|
||||||
|
|
||||||
bundle install && rake install
|
|
||||||
```
|
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
|
|
||||||
Pull the repo with ```docker pull wpscanteam/wpscan```
|
Pull the repo with ```docker pull wpscanteam/wpscan```
|
||||||
|
|
||||||
Enumerating usernames
|
Enumerating usernames
|
||||||
```
|
|
||||||
|
```shell
|
||||||
docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u
|
docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u
|
||||||
```
|
```
|
||||||
|
|
||||||
Enumerating a range of usernames
|
Enumerating a range of usernames
|
||||||
```
|
|
||||||
|
```shell
|
||||||
docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-100
|
docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-100
|
||||||
```
|
```
|
||||||
|
|
||||||
** replace u1-100 with a range of your choice.
|
** replace u1-100 with a range of your choice.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings. If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
|
Full user documentation can be found here; https://github.com/wpscanteam/wpscan/wiki/WPScan-User-Documentation
|
||||||
|
|
||||||
|
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings.
|
||||||
|
|
||||||
|
If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
|
||||||
As a result, when using the ```--enumerate``` option, don't forget to set the ```--plugins-detection``` accordingly, as its default is 'passive'.
|
As a result, when using the ```--enumerate``` option, don't forget to set the ```--plugins-detection``` accordingly, as its default is 'passive'.
|
||||||
|
|
||||||
For more options, open a terminal and type ```wpscan --help``` (if you built wpscan from the source, you should type the command outside of the git repo)
|
For more options, open a terminal and type ```wpscan --help``` (if you built wpscan from the source, you should type the command outside of the git repo)
|
||||||
|
|
||||||
The DB is located at ~/.wpscan/db
|
The DB is located at ~/.wpscan/db
|
||||||
|
|
||||||
|
## Optional: WordPress Vulnerability Database API
|
||||||
|
|
||||||
|
The WPScan CLI tool uses the [WordPress Vulnerability Database API](https://wpscan.com/api) to retrieve WordPress vulnerability data in real time. For WPScan to retrieve the vulnerability data an API token must be supplied via the `--api-token` option, or via a configuration file, as discussed below. An API token can be obtained by registering an account on [WPScan.com](https://wpscan.com/register).
|
||||||
|
|
||||||
|
Up to **25** API requests per day are given free of charge, that should be suitable to scan most WordPress websites at least once per day. When the daily 25 API requests are exhausted, WPScan will continue to work as normal but without any vulnerability data.
|
||||||
|
|
||||||
|
### How many API requests do you need?
|
||||||
|
|
||||||
|
- Our WordPress scanner makes one API request for the WordPress version, one request per installed plugin and one request per installed theme.
|
||||||
|
- On average, a WordPress website has 22 installed plugins.
|
||||||
|
|
||||||
|
## Load CLI options from file/s
|
||||||
|
|
||||||
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
|
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
|
||||||
|
|
||||||
* ~/.wpscan/cli_options.json
|
- ~/.wpscan/scan.json
|
||||||
* ~/.wpscan/cli_options.yml
|
- ~/.wpscan/scan.yml
|
||||||
* pwd/.wpscan/cli_options.json
|
- pwd/.wpscan/scan.json
|
||||||
* pwd/.wpscan/cli_options.yml
|
- pwd/.wpscan/scan.yml
|
||||||
|
|
||||||
If those files exist, options from them will be loaded and overridden if found twice.
|
If those files exist, options from the `cli_options` key will be loaded and overridden if found twice.
|
||||||
|
|
||||||
e.g:
|
e.g:
|
||||||
|
|
||||||
~/.wpscan/cli_options.yml:
|
~/.wpscan/scan.yml:
|
||||||
```
|
|
||||||
proxy: 'http://127.0.0.1:8080'
|
```yml
|
||||||
verbose: true
|
cli_options:
|
||||||
|
proxy: 'http://127.0.0.1:8080'
|
||||||
|
verbose: true
|
||||||
```
|
```
|
||||||
|
|
||||||
pwd/.wpscan/cli_options.yml:
|
pwd/.wpscan/scan.yml:
|
||||||
```
|
|
||||||
proxy: 'socks5://127.0.0.1:9090'
|
```yml
|
||||||
url: 'http://target.tld'
|
cli_options:
|
||||||
|
proxy: 'socks5://127.0.0.1:9090'
|
||||||
|
url: 'http://target.tld'
|
||||||
```
|
```
|
||||||
|
|
||||||
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
|
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
|
||||||
|
|
||||||
|
## Save API Token in a file
|
||||||
|
|
||||||
Enumerating usernames
|
The feature mentioned above is useful to keep the API Token in a config file and not have to supply it via the CLI each time. To do so, create the ~/.wpscan/scan.yml file containing the below:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
cli_options:
|
||||||
|
api_token: 'YOUR_API_TOKEN'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Load API Token From ENV (since v3.7.10)
|
||||||
|
|
||||||
|
The API Token will be automatically loaded from the ENV variable `WPSCAN_API_TOKEN` if present. If the `--api-token` CLI option is also provided, the value from the CLI will be used.
|
||||||
|
|
||||||
|
|
||||||
|
## Enumerating usernames
|
||||||
|
|
||||||
|
```shell
|
||||||
wpscan --url https://target.tld/ --enumerate u
|
wpscan --url https://target.tld/ --enumerate u
|
||||||
```
|
```
|
||||||
|
|
||||||
Enumerating a range of usernames
|
Enumerating a range of usernames
|
||||||
```
|
|
||||||
|
```shell
|
||||||
wpscan --url https://target.tld/ --enumerate u1-100
|
wpscan --url https://target.tld/ --enumerate u1-100
|
||||||
```
|
```
|
||||||
|
|
||||||
** replace u1-100 with a range of your choice.
|
** replace u1-100 with a range of your choice.
|
||||||
|
|
||||||
|
|
||||||
# PROJECT HOME
|
|
||||||
|
|
||||||
[https://wpscan.org](https://wpscan.org)
|
|
||||||
|
|
||||||
# VULNERABILITY DATABASE
|
|
||||||
|
|
||||||
[https://wpvulndb.com](https://wpvulndb.com)
|
|
||||||
|
|
||||||
# LICENSE
|
# LICENSE
|
||||||
|
|
||||||
## WPScan Public Source License
|
## WPScan Public Source License
|
||||||
|
|
||||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
|
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2019 WPScan Team.
|
||||||
|
|
||||||
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
||||||
|
|
||||||
@@ -115,7 +171,7 @@ Cases that include commercialization of WPScan require a commercial, non-free li
|
|||||||
|
|
||||||
1.2 "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
|
1.2 "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
|
||||||
|
|
||||||
1.3 "WPScan Team" means WPScan’s core developers, an updated list of whom can be found within the CREDITS file.
|
1.3 "WPScan Team" means WPScan’s core developers.
|
||||||
|
|
||||||
### 2. Commercialization
|
### 2. Commercialization
|
||||||
|
|
||||||
@@ -123,20 +179,18 @@ A commercial use is one intended for commercial advantage or monetary compensati
|
|||||||
|
|
||||||
Example cases of commercialization are:
|
Example cases of commercialization are:
|
||||||
|
|
||||||
- Using WPScan to provide commercial managed/Software-as-a-Service services.
|
- Using WPScan to provide commercial managed/Software-as-a-Service services.
|
||||||
- Distributing WPScan as a commercial product or as part of one.
|
- Distributing WPScan as a commercial product or as part of one.
|
||||||
- Using WPScan as a value added service/product.
|
- Using WPScan as a value added service/product.
|
||||||
|
|
||||||
Example cases which do not require a commercial license, and thus fall under the terms set out below, include (but are not limited to):
|
Example cases which do not require a commercial license, and thus fall under the terms set out below, include (but are not limited to):
|
||||||
|
|
||||||
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
|
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
|
||||||
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
|
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
|
||||||
- Using WPScan to test your own systems.
|
- Using WPScan to test your own systems.
|
||||||
- Any non-commercial use of WPScan.
|
- Any non-commercial use of WPScan.
|
||||||
|
|
||||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
|
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - contact@wpscan.com.
|
||||||
|
|
||||||
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
|
|
||||||
|
|
||||||
Free-use Terms and Conditions;
|
Free-use Terms and Conditions;
|
||||||
|
|
||||||
@@ -144,9 +198,9 @@ Free-use Terms and Conditions;
|
|||||||
|
|
||||||
Redistribution is permitted under the following conditions:
|
Redistribution is permitted under the following conditions:
|
||||||
|
|
||||||
- Unmodified License is provided with WPScan.
|
- Unmodified License is provided with WPScan.
|
||||||
- Unmodified Copyright notices are provided with WPScan.
|
- Unmodified Copyright notices are provided with WPScan.
|
||||||
- Does not conflict with the commercialization clause.
|
- Does not conflict with the commercialization clause.
|
||||||
|
|
||||||
### 4. Copying
|
### 4. Copying
|
||||||
|
|
||||||
|
|||||||
6
Rakefile
6
Rakefile
@@ -6,14 +6,18 @@ exec = []
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
require 'rubocop/rake_task'
|
require 'rubocop/rake_task'
|
||||||
|
|
||||||
RuboCop::RakeTask.new
|
RuboCop::RakeTask.new
|
||||||
|
|
||||||
exec << :rubocop
|
exec << :rubocop
|
||||||
rescue LoadError
|
rescue LoadError
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
require 'rspec/core/rake_task'
|
require 'rspec/core/rake_task'
|
||||||
RSpec::Core::RakeTask.new(:spec)
|
|
||||||
|
RSpec::Core::RakeTask.new(:spec) { |t| t.rspec_opts = %w{--tag ~slow} }
|
||||||
|
|
||||||
exec << :spec
|
exec << :spec
|
||||||
rescue LoadError
|
rescue LoadError
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'models'
|
require_relative 'models'
|
||||||
require_relative 'finders'
|
require_relative 'finders'
|
||||||
require_relative 'controllers'
|
require_relative 'controllers'
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'controllers/core'
|
require_relative 'controllers/core'
|
||||||
|
require_relative 'controllers/vuln_api'
|
||||||
require_relative 'controllers/custom_directories'
|
require_relative 'controllers/custom_directories'
|
||||||
require_relative 'controllers/wp_version'
|
require_relative 'controllers/wp_version'
|
||||||
require_relative 'controllers/main_theme'
|
require_relative 'controllers/main_theme'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Controller to add the aliases in the CLI
|
# Controller to add the aliases in the CLI
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Specific Core controller to include WordPress checks
|
# Specific Core controller to include WordPress checks
|
||||||
@@ -6,13 +8,13 @@ module WPScan
|
|||||||
def cli_options
|
def cli_options
|
||||||
[OptURL.new(['--url URL', 'The URL of the blog to scan'],
|
[OptURL.new(['--url URL', 'The URL of the blog to scan'],
|
||||||
required_unless: %i[update help hh version], default_protocol: 'http')] +
|
required_unless: %i[update help hh version], default_protocol: 'http')] +
|
||||||
super.drop(1) + # delete the --url from CMSScanner
|
super.drop(2) + # delete the --url and --force from CMSScanner
|
||||||
[
|
[
|
||||||
OptChoice.new(['--server SERVER', 'Force the supplied server module to be loaded'],
|
OptChoice.new(['--server SERVER', 'Force the supplied server module to be loaded'],
|
||||||
choices: %w[apache iis nginx],
|
choices: %w[apache iis nginx],
|
||||||
normalize: %i[downcase to_sym],
|
normalize: %i[downcase to_sym],
|
||||||
advanced: true),
|
advanced: true),
|
||||||
OptBoolean.new(['--force', 'Do not check if the target is running WordPress']),
|
OptBoolean.new(['--force', 'Do not check if the target is running WordPress or returns a 403']),
|
||||||
OptBoolean.new(['--[no-]update', 'Whether or not to update the Database'])
|
OptBoolean.new(['--[no-]update', 'Whether or not to update the Database'])
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -25,53 +27,56 @@ module WPScan
|
|||||||
# @return [ Boolean ]
|
# @return [ Boolean ]
|
||||||
def update_db_required?
|
def update_db_required?
|
||||||
if local_db.missing_files?
|
if local_db.missing_files?
|
||||||
raise MissingDatabaseFile if parsed_options[:update] == false
|
raise Error::MissingDatabaseFile if ParsedCli.update == false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
return parsed_options[:update] unless parsed_options[:update].nil?
|
return ParsedCli.update unless ParsedCli.update.nil?
|
||||||
|
|
||||||
return false unless user_interaction? && local_db.outdated?
|
return false unless user_interaction? && local_db.outdated?
|
||||||
|
|
||||||
output('@notice', msg: 'It seems like you have not updated the database for some time.')
|
output('@notice', msg: 'It seems like you have not updated the database for some time.')
|
||||||
print '[?] Do you want to update now? [Y]es [N]o, default: [N]'
|
print '[?] Do you want to update now? [Y]es [N]o, default: [N]'
|
||||||
|
|
||||||
Readline.readline =~ /^y/i ? true : false
|
/^y/i.match?(Readline.readline)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_db
|
def update_db
|
||||||
output('db_update_started')
|
output('db_update_started')
|
||||||
output('db_update_finished', updated: local_db.update, verbose: parsed_options[:verbose])
|
output('db_update_finished', updated: local_db.update, verbose: ParsedCli.verbose)
|
||||||
|
|
||||||
exit(0) unless parsed_options[:url]
|
exit(0) unless ParsedCli.url
|
||||||
end
|
end
|
||||||
|
|
||||||
def before_scan
|
def before_scan
|
||||||
@last_update = local_db.last_update
|
@last_update = local_db.last_update
|
||||||
|
|
||||||
maybe_output_banner_help_and_version # From CMS Scanner
|
maybe_output_banner_help_and_version # From CMSScanner
|
||||||
|
|
||||||
update_db if update_db_required?
|
update_db if update_db_required?
|
||||||
setup_cache
|
setup_cache
|
||||||
check_target_availability
|
check_target_availability
|
||||||
load_server_module
|
load_server_module
|
||||||
check_wordpress_state
|
check_wordpress_state
|
||||||
|
rescue Error::NotWordPress => e
|
||||||
|
target.maybe_add_cookies
|
||||||
|
raise e unless target.wordpress?(ParsedCli.detection_mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Raises errors if the target is hosted on wordpress.com or is not running WordPress
|
# Raises errors if the target is hosted on wordpress.com or is not running WordPress
|
||||||
# Also check if the homepage_url is still the install url
|
# Also check if the homepage_url is still the install url
|
||||||
def check_wordpress_state
|
def check_wordpress_state
|
||||||
raise WordPressHostedError if target.wordpress_hosted?
|
raise Error::WordPressHosted if target.wordpress_hosted?
|
||||||
|
|
||||||
if Addressable::URI.parse(target.homepage_url).path =~ %r{/wp-admin/install.php$}i
|
if %r{/wp-admin/install.php$}i.match?(Addressable::URI.parse(target.homepage_url).path)
|
||||||
|
|
||||||
output('not_fully_configured', url: target.homepage_url)
|
output('not_fully_configured', url: target.homepage_url)
|
||||||
|
|
||||||
exit(WPScan::ExitCode::VULNERABLE)
|
exit(WPScan::ExitCode::VULNERABLE)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise NotWordPressError unless target.wordpress? || parsed_options[:force]
|
raise Error::NotWordPress unless target.wordpress?(ParsedCli.detection_mode) || ParsedCli.force
|
||||||
end
|
end
|
||||||
|
|
||||||
# Loads the related server module in the target
|
# Loads the related server module in the target
|
||||||
@@ -83,7 +88,7 @@ module WPScan
|
|||||||
server = target.server || :Apache # Tries to auto detect the server
|
server = target.server || :Apache # Tries to auto detect the server
|
||||||
|
|
||||||
# Force a specific server module to be loaded if supplied
|
# Force a specific server module to be loaded if supplied
|
||||||
case parsed_options[:server]
|
case ParsedCli.server
|
||||||
when :apache
|
when :apache
|
||||||
server = :Apache
|
server = :Apache
|
||||||
when :iis
|
when :iis
|
||||||
@@ -95,7 +100,7 @@ module WPScan
|
|||||||
mod = CMSScanner::Target::Server.const_get(server)
|
mod = CMSScanner::Target::Server.const_get(server)
|
||||||
|
|
||||||
target.extend mod
|
target.extend mod
|
||||||
WPScan::WpItem.include mod
|
Model::WpItem.include mod
|
||||||
|
|
||||||
server
|
server
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Controller to ensure that the wp-content and wp-plugins
|
# Controller to ensure that the wp-content and wp-plugins
|
||||||
@@ -5,18 +7,18 @@ module WPScan
|
|||||||
class CustomDirectories < CMSScanner::Controller::Base
|
class CustomDirectories < CMSScanner::Controller::Base
|
||||||
def cli_options
|
def cli_options
|
||||||
[
|
[
|
||||||
OptString.new(['--wp-content-dir DIR']),
|
OptString.new(['--wp-content-dir DIR',
|
||||||
OptString.new(['--wp-plugins-dir DIR'])
|
'The wp-content directory if custom or not detected, such as "wp-content"']),
|
||||||
|
OptString.new(['--wp-plugins-dir DIR',
|
||||||
|
'The plugins directory if custom or not detected, such as "wp-content/plugins"'])
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def before_scan
|
def before_scan
|
||||||
target.content_dir = parsed_options[:wp_content_dir] if parsed_options[:wp_content_dir]
|
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
|
||||||
target.plugins_dir = parsed_options[:wp_plugins_dir] if parsed_options[:wp_plugins_dir]
|
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
|
||||||
|
|
||||||
return if target.content_dir
|
raise Error::WpContentDirNotDetected unless target.content_dir
|
||||||
|
|
||||||
raise 'Unable to identify the wp-content dir, please supply it with --wp-content-dir'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'enumeration/cli_options'
|
require_relative 'enumeration/cli_options'
|
||||||
require_relative 'enumeration/enum_methods'
|
require_relative 'enumeration/enum_methods'
|
||||||
|
|
||||||
@@ -5,13 +7,8 @@ module WPScan
|
|||||||
module Controller
|
module Controller
|
||||||
# Enumeration Controller
|
# Enumeration Controller
|
||||||
class Enumeration < CMSScanner::Controller::Base
|
class Enumeration < CMSScanner::Controller::Base
|
||||||
def before_scan
|
|
||||||
DB::DynamicFinders::Plugin.create_versions_finders
|
|
||||||
DB::DynamicFinders::Theme.create_versions_finders
|
|
||||||
end
|
|
||||||
|
|
||||||
def run
|
def run
|
||||||
enum = parsed_options[:enumerate] || {}
|
enum = ParsedCli.enumerate || {}
|
||||||
|
|
||||||
enum_plugins if enum_plugins?(enum)
|
enum_plugins if enum_plugins?(enum)
|
||||||
enum_themes if enum_themes?(enum)
|
enum_themes if enum_themes?(enum)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Enumeration CLI Options
|
# Enumeration CLI Options
|
||||||
@@ -9,7 +11,6 @@ module WPScan
|
|||||||
end
|
end
|
||||||
|
|
||||||
# @return [ Array<OptParseValidator::OptBase> ]
|
# @return [ Array<OptParseValidator::OptBase> ]
|
||||||
# rubocop:disable Metrics/MethodLength
|
|
||||||
def cli_enum_choices
|
def cli_enum_choices
|
||||||
[
|
[
|
||||||
OptMultiChoices.new(
|
OptMultiChoices.new(
|
||||||
@@ -17,10 +18,10 @@ module WPScan
|
|||||||
choices: {
|
choices: {
|
||||||
vp: OptBoolean.new(['--vulnerable-plugins']),
|
vp: OptBoolean.new(['--vulnerable-plugins']),
|
||||||
ap: OptBoolean.new(['--all-plugins']),
|
ap: OptBoolean.new(['--all-plugins']),
|
||||||
p: OptBoolean.new(['--plugins']),
|
p: OptBoolean.new(['--popular-plugins']),
|
||||||
vt: OptBoolean.new(['--vulnerable-themes']),
|
vt: OptBoolean.new(['--vulnerable-themes']),
|
||||||
at: OptBoolean.new(['--all-themes']),
|
at: OptBoolean.new(['--all-themes']),
|
||||||
t: OptBoolean.new(['--themes']),
|
t: OptBoolean.new(['--popular-themes']),
|
||||||
tt: OptBoolean.new(['--timthumbs']),
|
tt: OptBoolean.new(['--timthumbs']),
|
||||||
cb: OptBoolean.new(['--config-backups']),
|
cb: OptBoolean.new(['--config-backups']),
|
||||||
dbe: OptBoolean.new(['--db-exports']),
|
dbe: OptBoolean.new(['--db-exports']),
|
||||||
@@ -43,7 +44,6 @@ module WPScan
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/MethodLength
|
|
||||||
|
|
||||||
# @return [ Array<OptParseValidator::OptBase> ]
|
# @return [ Array<OptParseValidator::OptBase> ]
|
||||||
def cli_plugins_opts
|
def cli_plugins_opts
|
||||||
@@ -51,7 +51,7 @@ module WPScan
|
|||||||
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
|
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
|
||||||
OptChoice.new(
|
OptChoice.new(
|
||||||
['--plugins-detection MODE',
|
['--plugins-detection MODE',
|
||||||
'Use the supplied mode to enumerate Plugins, instead of the global (--detection-mode) mode.'],
|
'Use the supplied mode to enumerate Plugins.'],
|
||||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive
|
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive
|
||||||
),
|
),
|
||||||
OptBoolean.new(
|
OptBoolean.new(
|
||||||
@@ -62,9 +62,13 @@ module WPScan
|
|||||||
),
|
),
|
||||||
OptChoice.new(
|
OptChoice.new(
|
||||||
['--plugins-version-detection MODE',
|
['--plugins-version-detection MODE',
|
||||||
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
|
'Use the supplied mode to check plugins\' versions.'],
|
||||||
'or --plugins-detection modes.'],
|
|
||||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
||||||
|
),
|
||||||
|
OptInteger.new(
|
||||||
|
['--plugins-threshold THRESHOLD',
|
||||||
|
'Raise an error when the number of detected plugins via known locations reaches the threshold. ' \
|
||||||
|
'Set to 0 to ignore the threshold.'], default: 100, advanced: true
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -89,6 +93,11 @@ module WPScan
|
|||||||
'Use the supplied mode to check themes versions instead of the --detection-mode ' \
|
'Use the supplied mode to check themes versions instead of the --detection-mode ' \
|
||||||
'or --themes-detection modes.'],
|
'or --themes-detection modes.'],
|
||||||
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
|
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
|
||||||
|
),
|
||||||
|
OptInteger.new(
|
||||||
|
['--themes-threshold THRESHOLD',
|
||||||
|
'Raise an error when the number of detected themes via known locations reaches the threshold. ' \
|
||||||
|
'Set to 0 to ignore the threshold.'], default: 20, advanced: true
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -98,7 +107,7 @@ module WPScan
|
|||||||
[
|
[
|
||||||
OptFilePath.new(
|
OptFilePath.new(
|
||||||
['--timthumbs-list FILE-PATH', 'List of timthumbs\' location to use'],
|
['--timthumbs-list FILE-PATH', 'List of timthumbs\' location to use'],
|
||||||
exists: true, default: File.join(DB_DIR, 'timthumbs-v3.txt'), advanced: true
|
exists: true, default: DB_DIR.join('timthumbs-v3.txt').to_s, advanced: true
|
||||||
),
|
),
|
||||||
OptChoice.new(
|
OptChoice.new(
|
||||||
['--timthumbs-detection MODE',
|
['--timthumbs-detection MODE',
|
||||||
@@ -113,7 +122,7 @@ module WPScan
|
|||||||
[
|
[
|
||||||
OptFilePath.new(
|
OptFilePath.new(
|
||||||
['--config-backups-list FILE-PATH', 'List of config backups\' filenames to use'],
|
['--config-backups-list FILE-PATH', 'List of config backups\' filenames to use'],
|
||||||
exists: true, default: File.join(DB_DIR, 'config_backups.txt'), advanced: true
|
exists: true, default: DB_DIR.join('config_backups.txt').to_s, advanced: true
|
||||||
),
|
),
|
||||||
OptChoice.new(
|
OptChoice.new(
|
||||||
['--config-backups-detection MODE',
|
['--config-backups-detection MODE',
|
||||||
@@ -128,7 +137,7 @@ module WPScan
|
|||||||
[
|
[
|
||||||
OptFilePath.new(
|
OptFilePath.new(
|
||||||
['--db-exports-list FILE-PATH', 'List of DB exports\' paths to use'],
|
['--db-exports-list FILE-PATH', 'List of DB exports\' paths to use'],
|
||||||
exists: true, default: File.join(DB_DIR, 'db_exports.txt'), advanced: true
|
exists: true, default: DB_DIR.join('db_exports.txt').to_s, advanced: true
|
||||||
),
|
),
|
||||||
OptChoice.new(
|
OptChoice.new(
|
||||||
['--db-exports-detection MODE',
|
['--db-exports-detection MODE',
|
||||||
@@ -161,6 +170,12 @@ module WPScan
|
|||||||
['--users-detection MODE',
|
['--users-detection MODE',
|
||||||
'Use the supplied mode to enumerate Users, instead of the global (--detection-mode) mode.'],
|
'Use the supplied mode to enumerate Users, instead of the global (--detection-mode) mode.'],
|
||||||
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
|
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
|
||||||
|
),
|
||||||
|
OptRegexp.new(
|
||||||
|
[
|
||||||
|
'--exclude-usernames REGEXP_OR_STRING',
|
||||||
|
'Exclude usernames matching the Regexp/string (case insensitive). Regexp delimiters are not required.'
|
||||||
|
], options: Regexp::IGNORECASE
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,37 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Enumeration Methods
|
# Enumeration Methods
|
||||||
class Enumeration < CMSScanner::Controller::Base
|
class Enumeration < CMSScanner::Controller::Base
|
||||||
# @param [ String ] type (plugins or themes)
|
# @param [ String ] type (plugins or themes)
|
||||||
|
# @param [ Symbol ] detection_mode
|
||||||
#
|
#
|
||||||
# @return [ String ] The related enumration message depending on the parsed_options and type supplied
|
# @return [ String ] The related enumration message depending on the ParsedCli and type supplied
|
||||||
def enum_message(type)
|
def enum_message(type, detection_mode)
|
||||||
return unless %w[plugins themes].include?(type)
|
return unless %w[plugins themes].include?(type)
|
||||||
|
|
||||||
details = if parsed_options[:enumerate][:"vulnerable_#{type}"]
|
details = if ParsedCli.enumerate[:"vulnerable_#{type}"]
|
||||||
'Vulnerable'
|
'Vulnerable'
|
||||||
elsif parsed_options[:enumerate][:"all_#{type}"]
|
elsif ParsedCli.enumerate[:"all_#{type}"]
|
||||||
'All'
|
'All'
|
||||||
else
|
else
|
||||||
'Most Popular'
|
'Most Popular'
|
||||||
end
|
end
|
||||||
|
|
||||||
"Enumerating #{details} #{type.capitalize}"
|
"Enumerating #{details} #{type.capitalize} #{enum_detection_message(detection_mode)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [ Symbol ] detection_mode
|
||||||
|
#
|
||||||
|
# @return [ String ]
|
||||||
|
def enum_detection_message(detection_mode)
|
||||||
|
detection_method = if detection_mode == :mixed
|
||||||
|
'Passive and Aggressive'
|
||||||
|
else
|
||||||
|
detection_mode.to_s.capitalize
|
||||||
|
end
|
||||||
|
|
||||||
|
"(via #{detection_method} Methods)"
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] type (plugins, themes etc)
|
# @param [ String ] type (plugins, themes etc)
|
||||||
#
|
#
|
||||||
# @return [ Hash ]
|
# @return [ Hash ]
|
||||||
def default_opts(type)
|
def default_opts(type)
|
||||||
mode = parsed_options[:"#{type}_detection"] || parsed_options[:detection_mode]
|
mode = ParsedCli.options[:"#{type}_detection"] || ParsedCli.detection_mode
|
||||||
|
|
||||||
{
|
{
|
||||||
mode: mode,
|
mode: mode,
|
||||||
exclude_content: parsed_options[:exclude_content_based],
|
exclude_content: ParsedCli.exclude_content_based,
|
||||||
show_progression: user_interaction?,
|
show_progression: user_interaction?,
|
||||||
version_detection: {
|
version_detection: {
|
||||||
mode: parsed_options[:"#{type}_version_detection"] || mode,
|
mode: ParsedCli.options[:"#{type}_version_detection"] || mode,
|
||||||
confidence_threshold: parsed_options[:"#{type}_version_all"] ? 0 : 100
|
confidence_threshold: ParsedCli.options[:"#{type}_version_all"] ? 0 : 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -40,25 +56,29 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Boolean ] Wether or not to enumerate the plugins
|
# @return [ Boolean ] Wether or not to enumerate the plugins
|
||||||
def enum_plugins?(opts)
|
def enum_plugins?(opts)
|
||||||
opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
|
opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_plugins
|
def enum_plugins
|
||||||
opts = default_opts('plugins').merge(
|
opts = default_opts('plugins').merge(
|
||||||
list: plugins_list_from_opts(parsed_options),
|
list: plugins_list_from_opts(ParsedCli.options),
|
||||||
|
threshold: ParsedCli.plugins_threshold,
|
||||||
sort: true
|
sort: true
|
||||||
)
|
)
|
||||||
|
|
||||||
output('@info', msg: enum_message('plugins')) if user_interaction?
|
output('@info', msg: enum_message('plugins', opts[:mode])) if user_interaction?
|
||||||
# Enumerate the plugins & find their versions to avoid doing that when #version
|
# Enumerate the plugins & find their versions to avoid doing that when #version
|
||||||
# is called in the view
|
# is called in the view
|
||||||
plugins = target.plugins(opts)
|
plugins = target.plugins(opts)
|
||||||
|
|
||||||
output('@info', msg: 'Checking Plugin Versions') if user_interaction? && !plugins.empty?
|
if user_interaction? && !plugins.empty?
|
||||||
|
output('@info',
|
||||||
|
msg: "Checking Plugin Versions #{enum_detection_message(opts[:version_detection][:mode])}")
|
||||||
|
end
|
||||||
|
|
||||||
plugins.each(&:version)
|
plugins.each(&:version)
|
||||||
|
|
||||||
plugins.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_plugins]
|
plugins.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_plugins]
|
||||||
|
|
||||||
output('plugins', plugins: plugins)
|
output('plugins', plugins: plugins)
|
||||||
end
|
end
|
||||||
@@ -72,7 +92,7 @@ module WPScan
|
|||||||
|
|
||||||
if opts[:enumerate][:all_plugins]
|
if opts[:enumerate][:all_plugins]
|
||||||
DB::Plugins.all_slugs
|
DB::Plugins.all_slugs
|
||||||
elsif opts[:enumerate][:plugins]
|
elsif opts[:enumerate][:popular_plugins]
|
||||||
DB::Plugins.popular_slugs
|
DB::Plugins.popular_slugs
|
||||||
else
|
else
|
||||||
DB::Plugins.vulnerable_slugs
|
DB::Plugins.vulnerable_slugs
|
||||||
@@ -83,25 +103,29 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Boolean ] Wether or not to enumerate the themes
|
# @return [ Boolean ] Wether or not to enumerate the themes
|
||||||
def enum_themes?(opts)
|
def enum_themes?(opts)
|
||||||
opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
|
opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_themes
|
def enum_themes
|
||||||
opts = default_opts('themes').merge(
|
opts = default_opts('themes').merge(
|
||||||
list: themes_list_from_opts(parsed_options),
|
list: themes_list_from_opts(ParsedCli.options),
|
||||||
|
threshold: ParsedCli.themes_threshold,
|
||||||
sort: true
|
sort: true
|
||||||
)
|
)
|
||||||
|
|
||||||
output('@info', msg: enum_message('themes')) if user_interaction?
|
output('@info', msg: enum_message('themes', opts[:mode])) if user_interaction?
|
||||||
# Enumerate the themes & find their versions to avoid doing that when #version
|
# Enumerate the themes & find their versions to avoid doing that when #version
|
||||||
# is called in the view
|
# is called in the view
|
||||||
themes = target.themes(opts)
|
themes = target.themes(opts)
|
||||||
|
|
||||||
output('@info', msg: 'Checking Theme Versions') if user_interaction? && !themes.empty?
|
if user_interaction? && !themes.empty?
|
||||||
|
output('@info',
|
||||||
|
msg: "Checking Theme Versions #{enum_detection_message(opts[:version_detection][:mode])}")
|
||||||
|
end
|
||||||
|
|
||||||
themes.each(&:version)
|
themes.each(&:version)
|
||||||
|
|
||||||
themes.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_themes]
|
themes.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_themes]
|
||||||
|
|
||||||
output('themes', themes: themes)
|
output('themes', themes: themes)
|
||||||
end
|
end
|
||||||
@@ -115,7 +139,7 @@ module WPScan
|
|||||||
|
|
||||||
if opts[:enumerate][:all_themes]
|
if opts[:enumerate][:all_themes]
|
||||||
DB::Themes.all_slugs
|
DB::Themes.all_slugs
|
||||||
elsif opts[:enumerate][:themes]
|
elsif opts[:enumerate][:popular_themes]
|
||||||
DB::Themes.popular_slugs
|
DB::Themes.popular_slugs
|
||||||
else
|
else
|
||||||
DB::Themes.vulnerable_slugs
|
DB::Themes.vulnerable_slugs
|
||||||
@@ -123,31 +147,33 @@ module WPScan
|
|||||||
end
|
end
|
||||||
|
|
||||||
def enum_timthumbs
|
def enum_timthumbs
|
||||||
opts = default_opts('timthumbs').merge(list: parsed_options[:timthumbs_list])
|
opts = default_opts('timthumbs').merge(list: ParsedCli.timthumbs_list)
|
||||||
|
|
||||||
output('@info', msg: 'Enumerating Timthumbs') if user_interaction?
|
output('@info', msg: "Enumerating Timthumbs #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||||
output('timthumbs', timthumbs: target.timthumbs(opts))
|
output('timthumbs', timthumbs: target.timthumbs(opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_config_backups
|
def enum_config_backups
|
||||||
opts = default_opts('config_backups').merge(list: parsed_options[:config_backups_list])
|
opts = default_opts('config_backups').merge(list: ParsedCli.config_backups_list)
|
||||||
|
|
||||||
output('@info', msg: 'Enumerating Config Backups') if user_interaction?
|
output('@info', msg: "Enumerating Config Backups #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||||
output('config_backups', config_backups: target.config_backups(opts))
|
output('config_backups', config_backups: target.config_backups(opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_db_exports
|
def enum_db_exports
|
||||||
opts = default_opts('db_exports').merge(list: parsed_options[:db_exports_list])
|
opts = default_opts('db_exports').merge(list: ParsedCli.db_exports_list)
|
||||||
|
|
||||||
output('@info', msg: 'Enumerating DB Exports') if user_interaction?
|
output('@info', msg: "Enumerating DB Exports #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||||
output('db_exports', db_exports: target.db_exports(opts))
|
output('db_exports', db_exports: target.db_exports(opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_medias
|
def enum_medias
|
||||||
opts = default_opts('medias').merge(range: parsed_options[:enumerate][:medias])
|
opts = default_opts('medias').merge(range: ParsedCli.enumerate[:medias])
|
||||||
|
|
||||||
if user_interaction?
|
if user_interaction?
|
||||||
output('@info', msg: 'Enumerating Medias (Permalink setting must be set to "Plain" for those to be detected)')
|
output('@info',
|
||||||
|
msg: "Enumerating Medias #{enum_detection_message(opts[:mode])} "\
|
||||||
|
'(Permalink setting must be set to "Plain" for those to be detected)')
|
||||||
end
|
end
|
||||||
|
|
||||||
output('medias', medias: target.medias(opts))
|
output('medias', medias: target.medias(opts))
|
||||||
@@ -157,16 +183,16 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Boolean ] Wether or not to enumerate the users
|
# @return [ Boolean ] Wether or not to enumerate the users
|
||||||
def enum_users?(opts)
|
def enum_users?(opts)
|
||||||
opts[:users] || (parsed_options[:passwords] && !parsed_options[:username] && !parsed_options[:usernames])
|
opts[:users] || (ParsedCli.passwords && !ParsedCli.username && !ParsedCli.usernames)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_users
|
def enum_users
|
||||||
opts = default_opts('users').merge(
|
opts = default_opts('users').merge(
|
||||||
range: enum_users_range,
|
range: enum_users_range,
|
||||||
list: parsed_options[:users_list]
|
list: ParsedCli.users_list
|
||||||
)
|
)
|
||||||
|
|
||||||
output('@info', msg: 'Enumerating Users') if user_interaction?
|
output('@info', msg: "Enumerating Users #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||||
output('users', users: target.users(opts))
|
output('users', users: target.users(opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -174,7 +200,7 @@ module WPScan
|
|||||||
# If the --enumerate is used, the default value is handled by the Option
|
# If the --enumerate is used, the default value is handled by the Option
|
||||||
# However, when using --passwords alone, the default has to be set by the code below
|
# However, when using --passwords alone, the default has to be set by the code below
|
||||||
def enum_users_range
|
def enum_users_range
|
||||||
parsed_options[:enumerate][:users] || cli_enum_choices[0].choices[:u].validate(nil)
|
ParsedCli.enumerate[:users] || cli_enum_choices[0].choices[:u].validate(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Main Theme Controller
|
# Main Theme Controller
|
||||||
@@ -16,9 +18,9 @@ module WPScan
|
|||||||
output(
|
output(
|
||||||
'theme',
|
'theme',
|
||||||
theme: target.main_theme(
|
theme: target.main_theme(
|
||||||
mode: parsed_options[:main_theme_detection] || parsed_options[:detection_mode]
|
mode: ParsedCli.main_theme_detection || ParsedCli.detection_mode
|
||||||
),
|
),
|
||||||
verbose: parsed_options[:verbose]
|
verbose: ParsedCli.verbose
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Password Attack Controller
|
# Password Attack Controller
|
||||||
@@ -15,33 +17,40 @@ module WPScan
|
|||||||
'Maximum number of passwords to send by request with XMLRPC multicall'],
|
'Maximum number of passwords to send by request with XMLRPC multicall'],
|
||||||
default: 500),
|
default: 500),
|
||||||
OptChoice.new(['--password-attack ATTACK',
|
OptChoice.new(['--password-attack ATTACK',
|
||||||
'Force the supplied attack to be used rather than automatically determining one.'],
|
'Force the supplied attack to be used rather than automatically determining one.',
|
||||||
|
'Multicall will only work against WP < 4.4'],
|
||||||
choices: %w[wp-login xmlrpc xmlrpc-multicall],
|
choices: %w[wp-login xmlrpc xmlrpc-multicall],
|
||||||
normalize: %i[downcase underscore to_sym])
|
normalize: %i[downcase underscore to_sym]),
|
||||||
|
OptString.new(['--login-uri URI', 'The URI of the login page if different from /wp-login.php'])
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def attack_opts
|
||||||
return unless parsed_options[:passwords]
|
@attack_opts ||= {
|
||||||
|
|
||||||
if user_interaction?
|
|
||||||
output('@info',
|
|
||||||
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
|
||||||
end
|
|
||||||
|
|
||||||
attack_opts = {
|
|
||||||
show_progression: user_interaction?,
|
show_progression: user_interaction?,
|
||||||
multicall_max_passwords: parsed_options[:multicall_max_passwords]
|
multicall_max_passwords: ParsedCli.multicall_max_passwords
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
return unless ParsedCli.passwords
|
||||||
|
|
||||||
begin
|
begin
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
attacker.attack(users, passwords(parsed_options[:passwords]), attack_opts) do |user|
|
if user_interaction?
|
||||||
|
output('@info',
|
||||||
|
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
||||||
|
end
|
||||||
|
|
||||||
|
attacker.attack(users, ParsedCli.passwords, attack_opts) do |user|
|
||||||
found << user
|
found << user
|
||||||
|
|
||||||
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
||||||
end
|
end
|
||||||
|
rescue Error::NoLoginInterfaceDetected => e
|
||||||
|
# TODO: Maybe output that in JSON as well.
|
||||||
|
output('@notice', msg: e.to_s) if user_interaction?
|
||||||
ensure
|
ensure
|
||||||
output('users', users: found)
|
output('users', users: found)
|
||||||
end
|
end
|
||||||
@@ -52,55 +61,67 @@ module WPScan
|
|||||||
@attacker ||= attacker_from_cli_options || attacker_from_automatic_detection
|
@attacker ||= attacker_from_cli_options || attacker_from_automatic_detection
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ WPScan::XMLRPC ]
|
# @return [ Model::XMLRPC ]
|
||||||
def xmlrpc
|
def xmlrpc
|
||||||
@xmlrpc ||= target.xmlrpc
|
@xmlrpc ||= target.xmlrpc
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ CMSScanner::Finders::Finder ]
|
# @return [ CMSScanner::Finders::Finder ]
|
||||||
def attacker_from_cli_options
|
def attacker_from_cli_options
|
||||||
return unless parsed_options[:password_attack]
|
return unless ParsedCli.password_attack
|
||||||
|
|
||||||
case parsed_options[:password_attack]
|
case ParsedCli.password_attack
|
||||||
when :wp_login
|
when :wp_login
|
||||||
WPScan::Finders::Passwords::WpLogin.new(target)
|
raise Error::NoLoginInterfaceDetected unless target.login_url
|
||||||
|
|
||||||
|
Finders::Passwords::WpLogin.new(target)
|
||||||
when :xmlrpc
|
when :xmlrpc
|
||||||
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
|
raise Error::XMLRPCNotDetected unless xmlrpc
|
||||||
|
|
||||||
|
Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||||
when :xmlrpc_multicall
|
when :xmlrpc_multicall
|
||||||
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
raise Error::XMLRPCNotDetected unless xmlrpc
|
||||||
|
|
||||||
|
Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Boolean ]
|
||||||
|
def xmlrpc_get_users_blogs_enabled?
|
||||||
|
if xmlrpc&.enabled? &&
|
||||||
|
xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
|
||||||
|
!xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
|
||||||
|
.run.body.match?(/>\s*405\s*</)
|
||||||
|
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ CMSScanner::Finders::Finder ]
|
# @return [ CMSScanner::Finders::Finder ]
|
||||||
def attacker_from_automatic_detection
|
def attacker_from_automatic_detection
|
||||||
if xmlrpc&.enabled? && xmlrpc.available_methods.include?('wp.getUsersBlogs')
|
if xmlrpc_get_users_blogs_enabled?
|
||||||
wp_version = target.wp_version
|
wp_version = target.wp_version
|
||||||
|
|
||||||
if wp_version && wp_version < '4.4'
|
if wp_version && wp_version < '4.4'
|
||||||
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||||
else
|
else
|
||||||
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
|
Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||||
end
|
end
|
||||||
|
elsif target.login_url
|
||||||
|
Finders::Passwords::WpLogin.new(target)
|
||||||
else
|
else
|
||||||
WPScan::Finders::Passwords::WpLogin.new(target)
|
raise Error::NoLoginInterfaceDetected
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ Array<Users> ] The users to brute force
|
# @return [ Array<Users> ] The users to brute force
|
||||||
def users
|
def users
|
||||||
return target.users unless parsed_options[:usernames]
|
return target.users unless ParsedCli.usernames
|
||||||
|
|
||||||
parsed_options[:usernames].reduce([]) do |acc, elem|
|
ParsedCli.usernames.reduce([]) do |acc, elem|
|
||||||
acc << CMSScanner::User.new(elem.chomp)
|
acc << Model::User.new(elem.chomp)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param [ String ] wordlist_path
|
|
||||||
#
|
|
||||||
# @return [ Array<String> ]
|
|
||||||
def passwords(wordlist_path)
|
|
||||||
@passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
|
|
||||||
acc << elem.chomp
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
35
app/controllers/vuln_api.rb
Normal file
35
app/controllers/vuln_api.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Controller
|
||||||
|
# Controller to handle the API token
|
||||||
|
class VulnApi < CMSScanner::Controller::Base
|
||||||
|
ENV_KEY = 'WPSCAN_API_TOKEN'
|
||||||
|
|
||||||
|
def cli_options
|
||||||
|
[
|
||||||
|
OptString.new(
|
||||||
|
['--api-token TOKEN',
|
||||||
|
'The WPScan API Token to display vulnerability data, available at https://wpscan.com/profile']
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def before_scan
|
||||||
|
return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
|
||||||
|
|
||||||
|
DB::VulnApi.token = ParsedCli.api_token || ENV[ENV_KEY]
|
||||||
|
|
||||||
|
api_status = DB::VulnApi.status
|
||||||
|
|
||||||
|
raise Error::InvalidApiToken if api_status['status'] == 'forbidden'
|
||||||
|
raise Error::ApiLimitReached if api_status['requests_remaining'] == 0
|
||||||
|
raise api_status['http_error'] if api_status['http_error']
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_scan
|
||||||
|
output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Controller
|
module Controller
|
||||||
# Wp Version Controller
|
# Wp Version Controller
|
||||||
@@ -15,15 +17,15 @@ module WPScan
|
|||||||
end
|
end
|
||||||
|
|
||||||
def before_scan
|
def before_scan
|
||||||
WPScan::DB::DynamicFinders::Wordpress.create_versions_finders
|
DB::DynamicFinders::Wordpress.create_versions_finders
|
||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
output(
|
output(
|
||||||
'version',
|
'version',
|
||||||
version: target.wp_version(
|
version: target.wp_version(
|
||||||
mode: parsed_options[:wp_version_detection] || parsed_options[:detection_mode],
|
mode: ParsedCli.wp_version_detection || ParsedCli.detection_mode,
|
||||||
confidence_threshold: parsed_options[:wp_version_all] ? 0 : 100,
|
confidence_threshold: ParsedCli.wp_version_all ? 0 : 100,
|
||||||
show_progression: user_interaction?
|
show_progression: user_interaction?
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'finders/interesting_findings'
|
require_relative 'finders/interesting_findings'
|
||||||
require_relative 'finders/wp_items'
|
require_relative 'finders/wp_items'
|
||||||
require_relative 'finders/wp_version'
|
require_relative 'finders/wp_version'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'config_backups/known_filenames'
|
require_relative 'config_backups/known_filenames'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module ConfigBackups
|
module ConfigBackups
|
||||||
@@ -13,11 +15,10 @@ module WPScan
|
|||||||
def aggressive(opts = {})
|
def aggressive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(potential_urls(opts), opts) do |res|
|
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
|
||||||
# Might need to improve that
|
|
||||||
next unless res.body =~ /define/i && res.body !~ /<\s?html/i
|
next unless res.body =~ /define/i && res.body !~ /<\s?html/i
|
||||||
|
|
||||||
found << WPScan::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
found << Model::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'db_exports/known_locations'
|
require_relative 'db_exports/known_locations'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module DbExports
|
module DbExports
|
||||||
# DB Exports finder
|
# DB Exports finder
|
||||||
# See https://github.com/wpscanteam/wpscan-v3/issues/62
|
|
||||||
class KnownLocations < CMSScanner::Finders::Finder
|
class KnownLocations < CMSScanner::Finders::Finder
|
||||||
include CMSScanner::Finders::Finder::Enumerator
|
include CMSScanner::Finders::Finder::Enumerator
|
||||||
|
|
||||||
|
def valid_response_codes
|
||||||
|
@valid_response_codes ||= [200, 206].freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE|ALTER) (?:TABLE|DATABASE)|INSERT INTO/.freeze
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ String ] :list
|
# @option opts [ String ] :list
|
||||||
# @option opts [ Boolean ] :show_progression
|
# @option opts [ Boolean ] :show_progression
|
||||||
@@ -14,32 +21,79 @@ module WPScan
|
|||||||
def aggressive(opts = {})
|
def aggressive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(potential_urls(opts), opts) do |res|
|
enumerate(potential_urls(opts), opts.merge(check_full_response: valid_response_codes)) do |res|
|
||||||
next unless res.code == 200 && res.body =~ /INSERT INTO/
|
if res.effective_url.end_with?('.zip')
|
||||||
|
next unless %r{\Aapplication/zip}i.match?(res.headers['Content-Type'])
|
||||||
|
else
|
||||||
|
next unless SQL_PATTERN.match?(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
found << WPScan::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def full_request_params
|
||||||
|
@full_request_params ||= { headers: { 'Range' => 'bytes=0-3000' } }
|
||||||
|
end
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ String ] :list Mandatory
|
# @option opts [ String ] :list Mandatory
|
||||||
#
|
#
|
||||||
# @return [ Hash ]
|
# @return [ Hash ]
|
||||||
def potential_urls(opts = {})
|
def potential_urls(opts = {})
|
||||||
urls = {}
|
urls = {}
|
||||||
domain_name = target.uri.host[/(^[\w|-]+)/, 1]
|
index = 0
|
||||||
|
|
||||||
File.open(opts[:list]).each_with_index do |path, index|
|
File.open(opts[:list]).each do |path|
|
||||||
path.gsub!('{domain_name}', domain_name)
|
path.chomp!
|
||||||
|
|
||||||
urls[target.url(path.chomp)] = index
|
if path.include?('{domain_name}')
|
||||||
|
urls[target.url(path.gsub('{domain_name}', domain_name))] = index
|
||||||
|
|
||||||
|
if domain_name != domain_name_with_sub
|
||||||
|
urls[target.url(path.gsub('{domain_name}', domain_name_with_sub))] = index + 1
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
urls[target.url(path)] = index
|
||||||
|
end
|
||||||
|
|
||||||
|
index += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
urls
|
urls
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def domain_name
|
||||||
|
@domain_name ||= if Resolv::AddressRegex.match?(target.uri.host)
|
||||||
|
target.uri.host
|
||||||
|
else
|
||||||
|
(PublicSuffix.domain(target.uri.host) || target.uri.host)[/(^[\w|-]+)/, 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def domain_name_with_sub
|
||||||
|
@domain_name_with_sub ||=
|
||||||
|
if Resolv::AddressRegex.match?(target.uri.host)
|
||||||
|
target.uri.host
|
||||||
|
else
|
||||||
|
parsed = PublicSuffix.parse(target.uri.host)
|
||||||
|
|
||||||
|
if parsed.subdomain
|
||||||
|
parsed.subdomain.gsub(".#{parsed.tld}", '')
|
||||||
|
elsif parsed.domain
|
||||||
|
parsed.domain.gsub(".#{parsed.tld}", '')
|
||||||
|
else
|
||||||
|
target.uri.host
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue PublicSuffix::DomainNotAllowed
|
||||||
|
@domain_name_with_sub = target.uri.host
|
||||||
|
end
|
||||||
|
|
||||||
def create_progress_bar(opts = {})
|
def create_progress_bar(opts = {})
|
||||||
super(opts.merge(title: ' Checking DB Exports -'))
|
super(opts.merge(title: ' Checking DB Exports -'))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'interesting_findings/readme'
|
require_relative 'interesting_findings/readme'
|
||||||
|
require_relative 'interesting_findings/wp_cron'
|
||||||
require_relative 'interesting_findings/multisite'
|
require_relative 'interesting_findings/multisite'
|
||||||
require_relative 'interesting_findings/debug_log'
|
require_relative 'interesting_findings/debug_log'
|
||||||
require_relative 'interesting_findings/backup_db'
|
require_relative 'interesting_findings/backup_db'
|
||||||
require_relative 'interesting_findings/mu_plugins'
|
require_relative 'interesting_findings/mu_plugins'
|
||||||
|
require_relative 'interesting_findings/php_disabled'
|
||||||
require_relative 'interesting_findings/registration'
|
require_relative 'interesting_findings/registration'
|
||||||
require_relative 'interesting_findings/tmm_db_migrate'
|
require_relative 'interesting_findings/tmm_db_migrate'
|
||||||
require_relative 'interesting_findings/upload_sql_dump'
|
require_relative 'interesting_findings/upload_sql_dump'
|
||||||
@@ -23,7 +27,7 @@ module WPScan
|
|||||||
%w[
|
%w[
|
||||||
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
|
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
|
||||||
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
|
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
|
||||||
UploadSQLDump EmergencyPwdResetScript
|
UploadSQLDump EmergencyPwdResetScript WPCron PHPDisabled
|
||||||
].each do |f|
|
].each do |f|
|
||||||
finders << InterestingFindings.const_get(f).new(target)
|
finders << InterestingFindings.const_get(f).new(target)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -6,17 +8,15 @@ module WPScan
|
|||||||
# @return [ InterestingFinding ]
|
# @return [ InterestingFinding ]
|
||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
path = 'wp-content/backup-db/'
|
path = 'wp-content/backup-db/'
|
||||||
url = target.url(path)
|
res = target.head_and_get(path, [200, 403])
|
||||||
res = Browser.get(url)
|
|
||||||
|
|
||||||
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
|
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
|
||||||
|
|
||||||
WPScan::BackupDB.new(
|
Model::BackupDB.new(
|
||||||
url,
|
target.url(path),
|
||||||
confidence: 70,
|
confidence: 70,
|
||||||
found_by: DIRECT_ACCESS,
|
found_by: DIRECT_ACCESS,
|
||||||
interesting_entries: target.directory_listing_entries(path),
|
interesting_entries: target.directory_listing_entries(path)
|
||||||
references: { url: 'https://github.com/wpscanteam/wpscan/issues/422' }
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -9,10 +11,7 @@ module WPScan
|
|||||||
|
|
||||||
return unless target.debug_log?(path)
|
return unless target.debug_log?(path)
|
||||||
|
|
||||||
WPScan::DebugLog.new(
|
Model::DebugLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
target.url(path),
|
|
||||||
confidence: 100, found_by: DIRECT_ACCESS
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -5,17 +7,11 @@ module WPScan
|
|||||||
class DuplicatorInstallerLog < CMSScanner::Finders::Finder
|
class DuplicatorInstallerLog < CMSScanner::Finders::Finder
|
||||||
# @return [ InterestingFinding ]
|
# @return [ InterestingFinding ]
|
||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
url = target.url('installer-log.txt')
|
path = 'installer-log.txt'
|
||||||
res = Browser.get(url)
|
|
||||||
|
|
||||||
return unless res.body =~ /DUPLICATOR INSTALL-LOG/
|
return unless /DUPLICATOR(-|\s)?(PRO|LITE)?:? INSTALL-LOG/i.match?(target.head_and_get(path).body)
|
||||||
|
|
||||||
WPScan::DuplicatorInstallerLog.new(
|
Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -5,18 +7,15 @@ module WPScan
|
|||||||
class EmergencyPwdResetScript < CMSScanner::Finders::Finder
|
class EmergencyPwdResetScript < CMSScanner::Finders::Finder
|
||||||
# @return [ InterestingFinding ]
|
# @return [ InterestingFinding ]
|
||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
url = target.url('/emergency.php')
|
path = 'emergency.php'
|
||||||
res = Browser.get(url)
|
res = target.head_and_get(path)
|
||||||
|
|
||||||
return unless res.code == 200 && !target.homepage_or_404?(res)
|
return unless res.code == 200 && !target.homepage_or_404?(res)
|
||||||
|
|
||||||
WPScan::EmergencyPwdResetScript.new(
|
Model::EmergencyPwdResetScript.new(
|
||||||
url,
|
target.url(path),
|
||||||
confidence: res.body =~ /password/i ? 100 : 40,
|
confidence: /password/i.match?(res.body) ? 100 : 40,
|
||||||
found_by: DIRECT_ACCESS,
|
found_by: DIRECT_ACCESS
|
||||||
references: {
|
|
||||||
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -10,7 +12,7 @@ module WPScan
|
|||||||
|
|
||||||
return if fpd_entries.empty?
|
return if fpd_entries.empty?
|
||||||
|
|
||||||
WPScan::FullPathDisclosure.new(
|
Model::FullPathDisclosure.new(
|
||||||
target.url(path),
|
target.url(path),
|
||||||
confidence: 100,
|
confidence: 100,
|
||||||
found_by: DIRECT_ACCESS,
|
found_by: DIRECT_ACCESS,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -5,20 +7,16 @@ module WPScan
|
|||||||
class MuPlugins < CMSScanner::Finders::Finder
|
class MuPlugins < CMSScanner::Finders::Finder
|
||||||
# @return [ InterestingFinding ]
|
# @return [ InterestingFinding ]
|
||||||
def passive(_opts = {})
|
def passive(_opts = {})
|
||||||
pattern = %r{#{target.content_dir}/mu\-plugins/}i
|
pattern = %r{#{target.content_dir}/mu-plugins/}i
|
||||||
|
|
||||||
target.in_scope_urls(target.homepage_res) do |url|
|
target.in_scope_uris(target.homepage_res, '(//@href|//@src)[contains(., "mu-plugins")]') do |uri|
|
||||||
next unless Addressable::URI.parse(url).path =~ pattern
|
next unless uri.path&.match?(pattern)
|
||||||
|
|
||||||
url = target.url('wp-content/mu-plugins/')
|
url = target.url('wp-content/mu-plugins/')
|
||||||
|
|
||||||
return WPScan::MuPlugins.new(
|
target.mu_plugins = true
|
||||||
url,
|
|
||||||
confidence: 70,
|
return Model::MuPlugins.new(url, confidence: 70, found_by: 'URLs In Homepage (Passive Detection)')
|
||||||
found_by: 'URLs In Homepage (Passive Detection)',
|
|
||||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
|
||||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@@ -31,17 +29,9 @@ module WPScan
|
|||||||
return unless [200, 401, 403].include?(res.code)
|
return unless [200, 401, 403].include?(res.code)
|
||||||
return if target.homepage_or_404?(res)
|
return if target.homepage_or_404?(res)
|
||||||
|
|
||||||
# TODO: add the check for --exclude-content once implemented ?
|
|
||||||
|
|
||||||
target.mu_plugins = true
|
target.mu_plugins = true
|
||||||
|
|
||||||
WPScan::MuPlugins.new(
|
Model::MuPlugins.new(url, confidence: 80, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 80,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
|
||||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -10,18 +12,12 @@ module WPScan
|
|||||||
location = res.headers_hash['location']
|
location = res.headers_hash['location']
|
||||||
|
|
||||||
return unless [200, 302].include?(res.code)
|
return unless [200, 302].include?(res.code)
|
||||||
return if res.code == 302 && location =~ /wp-login\.php\?action=register/
|
return if res.code == 302 && location&.include?('wp-login.php?action=register')
|
||||||
return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
|
return unless res.code == 200 || (res.code == 302 && location&.include?('wp-signup.php'))
|
||||||
|
|
||||||
target.multisite = true
|
target.multisite = true
|
||||||
|
|
||||||
WPScan::Multisite.new(
|
Model::Multisite.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: 'This site seems to be a multisite',
|
|
||||||
references: { url: 'http://codex.wordpress.org/Glossary#Multisite' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
21
app/finders/interesting_findings/php_disabled.rb
Normal file
21
app/finders/interesting_findings/php_disabled.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Finders
|
||||||
|
module InterestingFindings
|
||||||
|
# See https://github.com/wpscanteam/wpscan/issues/1593
|
||||||
|
class PHPDisabled < CMSScanner::Finders::Finder
|
||||||
|
PATTERN = /\$wp_version =/.freeze
|
||||||
|
|
||||||
|
# @return [ InterestingFinding ]
|
||||||
|
def aggressive(_opts = {})
|
||||||
|
path = 'wp-includes/version.php'
|
||||||
|
|
||||||
|
return unless PATTERN.match?(target.head_and_get(path).body)
|
||||||
|
|
||||||
|
Model::PHPDisabled.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -5,14 +7,14 @@ module WPScan
|
|||||||
class Readme < CMSScanner::Finders::Finder
|
class Readme < CMSScanner::Finders::Finder
|
||||||
# @return [ InterestingFinding ]
|
# @return [ InterestingFinding ]
|
||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
potential_files.each do |file|
|
potential_files.each do |path|
|
||||||
url = target.url(file)
|
res = target.head_and_get(path)
|
||||||
res = Browser.get(url)
|
|
||||||
|
|
||||||
if res.code == 200 && res.body =~ /wordpress/i
|
next unless res.code == 200 && res.body =~ /wordpress/i
|
||||||
return WPScan::Readme.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
|
||||||
end
|
return Model::Readme.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -18,12 +20,7 @@ module WPScan
|
|||||||
|
|
||||||
target.registration_enabled = true
|
target.registration_enabled = true
|
||||||
|
|
||||||
WPScan::Registration.new(
|
Model::Registration.new(res.effective_url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
res.effective_url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: "Registration is enabled: #{res.effective_url}"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -7,16 +9,11 @@ module WPScan
|
|||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip'
|
path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip'
|
||||||
url = target.url(path)
|
url = target.url(path)
|
||||||
res = Browser.get(url)
|
res = browser.forge_request(url, target.head_or_get_request_params).run
|
||||||
|
|
||||||
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
||||||
|
|
||||||
WPScan::TmmDbMigrate.new(
|
Model::TmmDbMigrate.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
references: { packetstorm: 131_957 }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
@@ -11,12 +13,7 @@ module WPScan
|
|||||||
|
|
||||||
url = target.url(path)
|
url = target.url(path)
|
||||||
|
|
||||||
WPScan::UploadDirectoryListing.new(
|
Model::UploadDirectoryListing.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: "Upload directory has listing enabled: #{url}"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module InterestingFindings
|
module InterestingFindings
|
||||||
# UploadSQLDump finder
|
# UploadSQLDump finder
|
||||||
class UploadSQLDump < CMSScanner::Finders::Finder
|
class UploadSQLDump < CMSScanner::Finders::Finder
|
||||||
SQL_PATTERN = /(?:(?:(?:DROP|CREATE) TABLE)|INSERT INTO)/.freeze
|
SQL_PATTERN = /(?:DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO/.freeze
|
||||||
|
|
||||||
# @return [ InterestingFinding ]
|
# @return [ InterestingFinding ]
|
||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
url = dump_url
|
path = 'wp-content/uploads/dump.sql'
|
||||||
res = Browser.get(url)
|
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
|
||||||
|
|
||||||
return unless res.code == 200 && res.body =~ SQL_PATTERN
|
return unless SQL_PATTERN.match?(res.body)
|
||||||
|
|
||||||
WPScan::UploadSQLDump.new(
|
Model::UploadSQLDump.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def dump_url
|
|
||||||
target.url('wp-content/uploads/dump.sql')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
23
app/finders/interesting_findings/wp_cron.rb
Normal file
23
app/finders/interesting_findings/wp_cron.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Finders
|
||||||
|
module InterestingFindings
|
||||||
|
# wp-cron.php finder
|
||||||
|
class WPCron < CMSScanner::Finders::Finder
|
||||||
|
# @return [ InterestingFinding ]
|
||||||
|
def aggressive(_opts = {})
|
||||||
|
res = Browser.get(wp_cron_url)
|
||||||
|
|
||||||
|
return unless res.code == 200
|
||||||
|
|
||||||
|
Model::WPCron.new(wp_cron_url, confidence: 60, found_by: DIRECT_ACCESS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def wp_cron_url
|
||||||
|
@wp_cron_url ||= target.url('wp-cron.php')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
require_relative 'main_theme/css_style'
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'main_theme/css_style_in_homepage'
|
||||||
|
require_relative 'main_theme/css_style_in_404_page'
|
||||||
require_relative 'main_theme/woo_framework_meta_generator'
|
require_relative 'main_theme/woo_framework_meta_generator'
|
||||||
require_relative 'main_theme/urls_in_homepage'
|
require_relative 'main_theme/urls_in_homepage'
|
||||||
|
require_relative 'main_theme/urls_in_404_page'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
@@ -12,9 +16,11 @@ module WPScan
|
|||||||
# @param [ WPScan::Target ] target
|
# @param [ WPScan::Target ] target
|
||||||
def initialize(target)
|
def initialize(target)
|
||||||
finders <<
|
finders <<
|
||||||
MainTheme::CssStyle.new(target) <<
|
MainTheme::CssStyleInHomepage.new(target) <<
|
||||||
|
MainTheme::CssStyleIn404Page.new(target) <<
|
||||||
MainTheme::WooFrameworkMetaGenerator.new(target) <<
|
MainTheme::WooFrameworkMetaGenerator.new(target) <<
|
||||||
MainTheme::UrlsInHomepage.new(target)
|
MainTheme::UrlsInHomepage.new(target) <<
|
||||||
|
MainTheme::UrlsIn404Page.new(target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
14
app/finders/main_theme/css_style_in_404_page.rb
Normal file
14
app/finders/main_theme/css_style_in_404_page.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Finders
|
||||||
|
module MainTheme
|
||||||
|
# From the CSS style in the 404 page
|
||||||
|
class CssStyleIn404Page < CssStyleInHomepage
|
||||||
|
def passive(opts = {})
|
||||||
|
passive_from_css_href(target.error_404_res, opts) || passive_from_style_code(target.error_404_res, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module MainTheme
|
module MainTheme
|
||||||
# From the css style
|
# From the CSS style in the homepage
|
||||||
class CssStyle < CMSScanner::Finders::Finder
|
class CssStyleInHomepage < CMSScanner::Finders::Finder
|
||||||
include Finders::WpItems::URLsInHomepage
|
include Finders::WpItems::UrlsInPage # To have the item_code_pattern method available here
|
||||||
|
|
||||||
def create_theme(slug, style_url, opts)
|
def create_theme(slug, style_url, opts)
|
||||||
WPScan::Theme.new(
|
Model::Theme.new(
|
||||||
slug,
|
slug,
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by, confidence: 70, style_url: style_url)
|
opts.merge(found_by: found_by, confidence: 70, style_url: style_url)
|
||||||
@@ -18,10 +20,10 @@ module WPScan
|
|||||||
end
|
end
|
||||||
|
|
||||||
def passive_from_css_href(res, opts)
|
def passive_from_css_href(res, opts)
|
||||||
target.in_scope_urls(res, '//style/@src|//link/@href') do |url|
|
target.in_scope_uris(res, '//link/@href[contains(., "style.css")]') do |uri|
|
||||||
next unless Addressable::URI.parse(url).path =~ %r{/themes/([^\/]+)/style.css\z}i
|
next unless uri.path =~ %r{/themes/([^/]+)/style.css\z}i
|
||||||
|
|
||||||
return create_theme(Regexp.last_match[1], url, opts)
|
return create_theme(Regexp.last_match[1], uri.to_s, opts)
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@@ -31,7 +33,7 @@ module WPScan
|
|||||||
code = tag.text.to_s
|
code = tag.text.to_s
|
||||||
next if code.empty?
|
next if code.empty?
|
||||||
|
|
||||||
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
|
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'( ]*}i
|
||||||
|
|
||||||
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
|
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
|
||||||
end
|
end
|
||||||
15
app/finders/main_theme/urls_in_404_page.rb
Normal file
15
app/finders/main_theme/urls_in_404_page.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Finders
|
||||||
|
module MainTheme
|
||||||
|
# URLs In 404 Page Finder
|
||||||
|
class UrlsIn404Page < UrlsInHomepage
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.error_404_res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module MainTheme
|
module MainTheme
|
||||||
# URLs In Homepage Finder
|
# URLs In Homepage Finder
|
||||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||||
include WpItems::URLsInHomepage
|
include WpItems::UrlsInPage
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
@@ -11,14 +13,19 @@ module WPScan
|
|||||||
def passive(opts = {})
|
def passive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
slugs = items_from_links('themes', false) + items_from_codes('themes', false)
|
slugs = items_from_links('themes', uniq: false) + items_from_codes('themes', uniq: false)
|
||||||
|
|
||||||
slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
|
slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
|
||||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.homepage_res
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module MainTheme
|
module MainTheme
|
||||||
@@ -8,9 +10,9 @@ module WPScan
|
|||||||
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze
|
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze
|
||||||
|
|
||||||
def passive(opts = {})
|
def passive(opts = {})
|
||||||
return unless target.homepage_res.body =~ PATTERN
|
return unless target.homepage_res.body =~ PATTERN || target.error_404_res.body =~ PATTERN
|
||||||
|
|
||||||
WPScan::Theme.new(
|
Model::Theme.new(
|
||||||
Regexp.last_match[1],
|
Regexp.last_match[1],
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by, confidence: 80)
|
opts.merge(found_by: found_by, confidence: 80)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'medias/attachment_brute_forcing'
|
require_relative 'medias/attachment_brute_forcing'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Medias
|
module Medias
|
||||||
@@ -15,7 +17,7 @@ module WPScan
|
|||||||
enumerate(target_urls(opts), opts) do |res|
|
enumerate(target_urls(opts), opts) do |res|
|
||||||
next unless res.code == 200
|
next unless res.code == 200
|
||||||
|
|
||||||
found << WPScan::Media.new(res.effective_url, opts.merge(found_by: found_by, confidence: 100))
|
found << Model::Media.new(res.effective_url, opts.merge(found_by: found_by, confidence: 100))
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'passwords/wp_login'
|
require_relative 'passwords/wp_login'
|
||||||
require_relative 'passwords/xml_rpc'
|
require_relative 'passwords/xml_rpc'
|
||||||
require_relative 'passwords/xml_rpc_multicall'
|
require_relative 'passwords/xml_rpc_multicall'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Passwords
|
module Passwords
|
||||||
@@ -10,7 +12,8 @@ module WPScan
|
|||||||
end
|
end
|
||||||
|
|
||||||
def valid_credentials?(response)
|
def valid_credentials?(response)
|
||||||
response.code == 302
|
response.code == 302 &&
|
||||||
|
Array(response.headers['Set-Cookie'])&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
|
||||||
end
|
end
|
||||||
|
|
||||||
def errored_response?(response)
|
def errored_response?(response)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Passwords
|
module Passwords
|
||||||
@@ -6,15 +8,15 @@ module WPScan
|
|||||||
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
||||||
|
|
||||||
def login_request(username, password)
|
def login_request(username, password)
|
||||||
target.method_call('wp.getUsersBlogs', [username, password])
|
target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_credentials?(response)
|
def valid_credentials?(response)
|
||||||
response.code == 200 && response.body =~ /blogName/
|
response.code == 200 && response.body.include?('blogName')
|
||||||
end
|
end
|
||||||
|
|
||||||
def errored_response?(response)
|
def errored_response?(response)
|
||||||
response.code != 200 && response.body !~ /login_error/i
|
response.code != 200 && response.body !~ /Incorrect username or password/i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Passwords
|
module Passwords
|
||||||
@@ -17,32 +19,58 @@ module WPScan
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
target.multi_call(methods).run
|
target.multi_call(methods, cache_ttl: 0).run
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ Array<CMSScanner::User> ] users
|
# @param [ IO ] file
|
||||||
# @param [ Array<String> ] passwords
|
# @param [ Integer ] passwords_size
|
||||||
|
# @return [ Array<String> ] The passwords from the last checked position in the file until there are
|
||||||
|
# passwords_size passwords retrieved
|
||||||
|
def passwords_from_wordlist(file, passwords_size)
|
||||||
|
pwds = []
|
||||||
|
added_pwds = 0
|
||||||
|
|
||||||
|
return pwds if passwords_size.zero?
|
||||||
|
|
||||||
|
# Make sure that the main code does not call #sysseek or #count etc
|
||||||
|
# otherwise the file descriptor will be set to somwehere else
|
||||||
|
file.each_line(chomp: true) do |line|
|
||||||
|
pwds << line
|
||||||
|
added_pwds += 1
|
||||||
|
|
||||||
|
break if added_pwds == passwords_size
|
||||||
|
end
|
||||||
|
|
||||||
|
pwds
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [ Array<Model::User> ] users
|
||||||
|
# @param [ String ] wordlist_path
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ Boolean ] :show_progression
|
# @option opts [ Boolean ] :show_progression
|
||||||
# @option opts [ Integer ] :multicall_max_passwords
|
# @option opts [ Integer ] :multicall_max_passwords
|
||||||
#
|
#
|
||||||
# @yield [ CMSScanner::User ] When a valid combination is found
|
# @yield [ Model::User ] When a valid combination is found
|
||||||
#
|
#
|
||||||
# TODO: Make rubocop happy about metrics etc
|
# TODO: Make rubocop happy about metrics etc
|
||||||
#
|
#
|
||||||
# rubocop:disable all
|
# rubocop:disable all
|
||||||
def attack(users, passwords, opts = {})
|
def attack(users, wordlist_path, opts = {})
|
||||||
wordlist_index = 0
|
checked_passwords = 0
|
||||||
|
wordlist = File.open(wordlist_path)
|
||||||
|
wordlist_size = wordlist.count
|
||||||
max_passwords = opts[:multicall_max_passwords]
|
max_passwords = opts[:multicall_max_passwords]
|
||||||
current_passwords_size = passwords_size(max_passwords, users.size)
|
current_passwords_size = passwords_size(max_passwords, users.size)
|
||||||
|
|
||||||
create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
|
create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil,
|
||||||
show_progression: opts[:show_progression])
|
show_progression: opts[:show_progression])
|
||||||
|
|
||||||
|
wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
current_users = users.select { |user| user.password.nil? }
|
current_users = users.select { |user| user.password.nil? }
|
||||||
current_passwords = passwords[wordlist_index, current_passwords_size]
|
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
|
||||||
wordlist_index += current_passwords_size
|
checked_passwords += current_passwords_size
|
||||||
|
|
||||||
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
||||||
|
|
||||||
@@ -74,16 +102,19 @@ module WPScan
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
|
begin
|
||||||
|
progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
|
||||||
|
rescue ProgressBar::InvalidProgressError
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# Maybe a progress_bar.stop ?
|
# Maybe a progress_bar.stop ?
|
||||||
end
|
end
|
||||||
# rubocop:disable all
|
# rubocop:enable all
|
||||||
|
|
||||||
def passwords_size(max_passwords, users_size)
|
def passwords_size(max_passwords, users_size)
|
||||||
return 1 if max_passwords < users_size
|
return 1 if max_passwords < users_size
|
||||||
return 0 if users_size == 0
|
return 0 if users_size.zero?
|
||||||
|
|
||||||
max_passwords / users_size
|
max_passwords / users_size
|
||||||
end
|
end
|
||||||
@@ -92,9 +123,13 @@ module WPScan
|
|||||||
def check_and_output_errors(res)
|
def check_and_output_errors(res)
|
||||||
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
||||||
|
|
||||||
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
|
if /parse error. not well formed/i.match?(res.body)
|
||||||
|
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')
|
||||||
|
end
|
||||||
|
|
||||||
progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
|
return unless /requested method [^ ]+ does not exist/i.match?(res.body)
|
||||||
|
|
||||||
|
progress_bar.log('The requested method is not supported')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'plugin_version/readme'
|
require_relative 'plugin_version/readme'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
@@ -7,29 +9,19 @@ module WPScan
|
|||||||
class Base
|
class Base
|
||||||
include CMSScanner::Finders::UniqueFinder
|
include CMSScanner::Finders::UniqueFinder
|
||||||
|
|
||||||
# @param [ WPScan::Plugin ] plugin
|
# @param [ Model::Plugin ] plugin
|
||||||
def initialize(plugin)
|
def initialize(plugin)
|
||||||
finders << PluginVersion::Readme.new(plugin)
|
finders << PluginVersion::Readme.new(plugin)
|
||||||
|
|
||||||
load_specific_finders(plugin)
|
create_and_load_dynamic_versions_finders(plugin)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load the finders associated with the plugin
|
# Create the dynamic version finders related to the plugin and register them
|
||||||
#
|
#
|
||||||
# @param [ WPScan::Plugin ] plugin
|
# @param [ Model::Plugin ] plugin
|
||||||
def load_specific_finders(plugin)
|
def create_and_load_dynamic_versions_finders(plugin)
|
||||||
module_name = plugin.classify
|
DB::DynamicFinders::Plugin.create_versions_finders(plugin.slug).each do |finder|
|
||||||
|
finders << finder.new(plugin)
|
||||||
return unless Finders::PluginVersion.constants.include?(module_name)
|
|
||||||
|
|
||||||
mod = Finders::PluginVersion.const_get(module_name)
|
|
||||||
|
|
||||||
mod.constants.each do |constant|
|
|
||||||
c = mod.const_get(constant)
|
|
||||||
|
|
||||||
next unless c.is_a?(Class)
|
|
||||||
|
|
||||||
finders << c.new(plugin)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module PluginVersion
|
module PluginVersion
|
||||||
@@ -7,21 +9,23 @@ module WPScan
|
|||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
found_by_msg = 'Readme - %s (Aggressive Detection)'
|
found_by_msg = 'Readme - %s (Aggressive Detection)'
|
||||||
|
|
||||||
WPScan::WpItem::READMES.each do |file|
|
# The target(plugin)#readme_url can't be used directly here
|
||||||
url = target.url(file)
|
# as if the --detection-mode is passive, it will always return nil
|
||||||
res = Browser.get(url)
|
target.potential_readme_filenames.each do |file|
|
||||||
|
res = target.head_and_get(file)
|
||||||
|
|
||||||
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
|
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
|
||||||
|
|
||||||
return numbers.reduce([]) do |a, e|
|
return numbers.reduce([]) do |a, e|
|
||||||
a << WPScan::Version.new(
|
a << Model::Version.new(
|
||||||
e[0],
|
e[0],
|
||||||
found_by: format(found_by_msg, e[1]),
|
found_by: format(found_by_msg, e[1]),
|
||||||
confidence: e[2],
|
confidence: e[2],
|
||||||
interesting_entries: [url]
|
interesting_entries: [res.effective_url]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -44,31 +48,29 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ String, nil ] The version number detected from the stable tag
|
# @return [ String, nil ] The version number detected from the stable tag
|
||||||
def from_stable_tag(body)
|
def from_stable_tag(body)
|
||||||
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
|
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i
|
||||||
|
|
||||||
number = Regexp.last_match[1]
|
number = Regexp.last_match[1]
|
||||||
|
|
||||||
number if number =~ /[0-9]+/
|
number if /[0-9]+/.match?(number)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] body
|
# @param [ String ] body
|
||||||
#
|
#
|
||||||
# @return [ String, nil ] The best version number detected from the changelog section
|
# @return [ String, nil ] The best version number detected from the changelog section
|
||||||
def from_changelog_section(body)
|
def from_changelog_section(body)
|
||||||
extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
|
extracted_versions = body.scan(/^=+\s+(?:v(?:ersion)?\s*)?([0-9.-]+)[^=]*=+$/i)
|
||||||
|
|
||||||
return if extracted_versions.nil? || extracted_versions.empty?
|
return if extracted_versions.nil? || extracted_versions.empty?
|
||||||
|
|
||||||
extracted_versions.flatten!
|
extracted_versions.flatten!
|
||||||
# must contain at least one number
|
# must contain at least one number
|
||||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
extracted_versions = extracted_versions.grep(/[0-9]+/)
|
||||||
|
|
||||||
sorted = extracted_versions.sort do |x, y|
|
sorted = extracted_versions.sort do |x, y|
|
||||||
begin
|
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
rescue StandardError
|
||||||
rescue StandardError
|
0
|
||||||
0
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
sorted.last
|
sorted.last
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'plugins/urls_in_homepage'
|
require_relative 'plugins/urls_in_homepage'
|
||||||
|
require_relative 'plugins/urls_in_404_page'
|
||||||
require_relative 'plugins/known_locations'
|
require_relative 'plugins/known_locations'
|
||||||
# From the DynamicFinders
|
# From the DynamicFinders
|
||||||
require_relative 'plugins/comment'
|
require_relative 'plugins/comment'
|
||||||
@@ -20,6 +23,7 @@ module WPScan
|
|||||||
def initialize(target)
|
def initialize(target)
|
||||||
finders <<
|
finders <<
|
||||||
Plugins::UrlsInHomepage.new(target) <<
|
Plugins::UrlsInHomepage.new(target) <<
|
||||||
|
Plugins::UrlsIn404Page.new(target) <<
|
||||||
Plugins::HeaderPattern.new(target) <<
|
Plugins::HeaderPattern.new(target) <<
|
||||||
Plugins::Comment.new(target) <<
|
Plugins::Comment.new(target) <<
|
||||||
Plugins::Xpath.new(target) <<
|
Plugins::Xpath.new(target) <<
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# Plugins finder from Dynamic Finder 'BodyPattern'
|
# Plugins finder from Dynamic Finder 'BodyPattern'
|
||||||
class BodyPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
class BodyPattern < Finders::DynamicFinder::WpItems::Finder
|
||||||
DEFAULT_CONFIDENCE = 30
|
DEFAULT_CONFIDENCE = 30
|
||||||
|
|
||||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||||
@@ -13,9 +15,9 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||||
def process_response(opts, response, slug, klass, config)
|
def process_response(opts, response, slug, klass, config)
|
||||||
return unless response.body =~ config['pattern']
|
return unless response.body&.match?(config['pattern'])
|
||||||
|
|
||||||
Plugin.new(
|
Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# Plugins finder from the Dynamic Finder 'Comment'
|
# Plugins finder from the Dynamic Finder 'Comment'
|
||||||
class Comment < WPScan::Finders::DynamicFinder::WpItems::Finder
|
class Comment < Finders::DynamicFinder::WpItems::Finder
|
||||||
DEFAULT_CONFIDENCE = 30
|
DEFAULT_CONFIDENCE = 30
|
||||||
|
|
||||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||||
@@ -16,9 +18,9 @@ module WPScan
|
|||||||
response.html.xpath(config['xpath'] || '//comment()').each do |node|
|
response.html.xpath(config['xpath'] || '//comment()').each do |node|
|
||||||
comment = node.text.to_s.strip
|
comment = node.text.to_s.strip
|
||||||
|
|
||||||
next unless comment =~ config['pattern']
|
next unless comment&.match?(config['pattern'])
|
||||||
|
|
||||||
return Plugin.new(
|
return Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# Plugins finder from Dynamic Finder 'ConfigParser'
|
# Plugins finder from Dynamic Finder 'ConfigParser'
|
||||||
class ConfigParser < WPScan::Finders::DynamicFinder::WpItems::Finder
|
class ConfigParser < Finders::DynamicFinder::WpItems::Finder
|
||||||
DEFAULT_CONFIDENCE = 40
|
DEFAULT_CONFIDENCE = 40
|
||||||
|
|
||||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||||
@@ -19,7 +21,7 @@ module WPScan
|
|||||||
# when checking for plugins
|
# when checking for plugins
|
||||||
#
|
#
|
||||||
|
|
||||||
Plugin.new(
|
Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# Plugins finder from Dynamic Finder 'HeaderPattern'
|
# Plugins finder from Dynamic Finder 'HeaderPattern'
|
||||||
class HeaderPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
class HeaderPattern < Finders::DynamicFinder::WpItems::Finder
|
||||||
DEFAULT_CONFIDENCE = 30
|
DEFAULT_CONFIDENCE = 30
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
@@ -18,7 +20,7 @@ module WPScan
|
|||||||
configs.each do |klass, config|
|
configs.each do |klass, config|
|
||||||
next unless headers[config['header']] && headers[config['header']].to_s =~ config['pattern']
|
next unless headers[config['header']] && headers[config['header']].to_s =~ config['pattern']
|
||||||
|
|
||||||
found << Plugin.new(
|
found << Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# Plugins finder from the Dynamic Finder 'JavascriptVar'
|
# Plugins finder from the Dynamic Finder 'JavascriptVar'
|
||||||
class JavascriptVar < WPScan::Finders::DynamicFinder::WpItems::Finder
|
class JavascriptVar < Finders::DynamicFinder::WpItems::Finder
|
||||||
DEFAULT_CONFIDENCE = 60
|
DEFAULT_CONFIDENCE = 60
|
||||||
|
|
||||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||||
@@ -16,7 +18,7 @@ module WPScan
|
|||||||
response.html.xpath(config['xpath'] || '//script[not(@src)]').each do |node|
|
response.html.xpath(config['xpath'] || '//script[not(@src)]').each do |node|
|
||||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||||
|
|
||||||
return Plugin.new(
|
return Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
@@ -5,6 +7,11 @@ module WPScan
|
|||||||
class KnownLocations < CMSScanner::Finders::Finder
|
class KnownLocations < CMSScanner::Finders::Finder
|
||||||
include CMSScanner::Finders::Finder::Enumerator
|
include CMSScanner::Finders::Finder::Enumerator
|
||||||
|
|
||||||
|
# @return [ Array<Integer> ]
|
||||||
|
def valid_response_codes
|
||||||
|
@valid_response_codes ||= [200, 401, 403, 500].freeze
|
||||||
|
end
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ String ] :list
|
# @option opts [ String ] :list
|
||||||
#
|
#
|
||||||
@@ -12,12 +19,14 @@ module WPScan
|
|||||||
def aggressive(opts = {})
|
def aggressive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(target_urls(opts), opts) do |res, slug|
|
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug|
|
||||||
# TODO: follow the location (from enumerate()) and remove the 301 here ?
|
finding_opts = opts.merge(found_by: found_by,
|
||||||
# As a result, it might remove false positive due to redirection to the homepage
|
confidence: 80,
|
||||||
next unless [200, 401, 403, 301].include?(res.code)
|
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
|
||||||
|
|
||||||
found << WPScan::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
found << Model::Plugin.new(slug, target, finding_opts)
|
||||||
|
|
||||||
|
raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
@@ -30,10 +39,9 @@ module WPScan
|
|||||||
def target_urls(opts = {})
|
def target_urls(opts = {})
|
||||||
slugs = opts[:list] || DB::Plugins.vulnerable_slugs
|
slugs = opts[:list] || DB::Plugins.vulnerable_slugs
|
||||||
urls = {}
|
urls = {}
|
||||||
plugins_url = target.plugins_url
|
|
||||||
|
|
||||||
slugs.each do |slug|
|
slugs.each do |slug|
|
||||||
urls["#{plugins_url}#{URI.encode(slug)}/"] = slug
|
urls[target.plugin_url(slug)] = slug
|
||||||
end
|
end
|
||||||
|
|
||||||
urls
|
urls
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# Plugins finder from Dynamic Finder 'QueryParameter'
|
# Plugins finder from Dynamic Finder 'QueryParameter'
|
||||||
class QueryParameter < WPScan::Finders::DynamicFinder::WpItems::Finder
|
class QueryParameter < Finders::DynamicFinder::WpItems::Finder
|
||||||
DEFAULT_CONFIDENCE = 10
|
DEFAULT_CONFIDENCE = 10
|
||||||
|
|
||||||
def passive(_opts = {})
|
def passive(_opts = {})
|
||||||
|
|||||||
16
app/finders/plugins/urls_in_404_page.rb
Normal file
16
app/finders/plugins/urls_in_404_page.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Finders
|
||||||
|
module Plugins
|
||||||
|
# URLs In 404 Page Finder
|
||||||
|
# Typically, the items detected from URLs like /wp-content/plugins/<slug>/
|
||||||
|
class UrlsIn404Page < UrlsInHomepage
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.error_404_res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# URLs In Homepage Finder
|
# URLs In Homepage Finder
|
||||||
# Typically, the items detected from URLs like
|
# Typically, the items detected from URLs like /wp-content/plugins/<slug>/
|
||||||
# /wp-content/plugins/<slug>/
|
|
||||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||||
include WpItems::URLsInHomepage
|
include WpItems::UrlsInPage
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
@@ -14,11 +15,16 @@ module WPScan
|
|||||||
found = []
|
found = []
|
||||||
|
|
||||||
(items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.each do |slug|
|
(items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.each do |slug|
|
||||||
found << Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.homepage_res
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# Plugins finder from the Dynamic Finder 'Xpath'
|
# Plugins finder from the Dynamic Finder 'Xpath'
|
||||||
class Xpath < WPScan::Finders::DynamicFinder::WpItems::Finder
|
class Xpath < Finders::DynamicFinder::WpItems::Finder
|
||||||
DEFAULT_CONFIDENCE = 40
|
DEFAULT_CONFIDENCE = 40
|
||||||
|
|
||||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||||
@@ -16,7 +18,7 @@ module WPScan
|
|||||||
response.html.xpath(config['xpath']).each do |node|
|
response.html.xpath(config['xpath']).each do |node|
|
||||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||||
|
|
||||||
return Plugin.new(
|
return Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
target,
|
target,
|
||||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'theme_version/style'
|
require_relative 'theme_version/style'
|
||||||
require_relative 'theme_version/woo_framework_meta_generator'
|
require_relative 'theme_version/woo_framework_meta_generator'
|
||||||
|
|
||||||
@@ -8,31 +10,21 @@ module WPScan
|
|||||||
class Base
|
class Base
|
||||||
include CMSScanner::Finders::UniqueFinder
|
include CMSScanner::Finders::UniqueFinder
|
||||||
|
|
||||||
# @param [ WPScan::Theme ] theme
|
# @param [ Model::Theme ] theme
|
||||||
def initialize(theme)
|
def initialize(theme)
|
||||||
finders <<
|
finders <<
|
||||||
ThemeVersion::Style.new(theme) <<
|
ThemeVersion::Style.new(theme) <<
|
||||||
ThemeVersion::WooFrameworkMetaGenerator.new(theme)
|
ThemeVersion::WooFrameworkMetaGenerator.new(theme)
|
||||||
|
|
||||||
load_specific_finders(theme)
|
create_and_load_dynamic_versions_finders(theme)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load the finders associated with the theme
|
# Create the dynamic version finders related to the theme and register them
|
||||||
#
|
#
|
||||||
# @param [ WPScan::Theme ] theme
|
# @param [ Model::Theme ] theme
|
||||||
def load_specific_finders(theme)
|
def create_and_load_dynamic_versions_finders(theme)
|
||||||
module_name = theme.classify
|
DB::DynamicFinders::Theme.create_versions_finders(theme.slug).each do |finder|
|
||||||
|
finders << finder.new(theme)
|
||||||
return unless Finders::ThemeVersion.constants.include?(module_name)
|
|
||||||
|
|
||||||
mod = Finders::ThemeVersion.const_get(module_name)
|
|
||||||
|
|
||||||
mod.constants.each do |constant|
|
|
||||||
c = mod.const_get(constant)
|
|
||||||
|
|
||||||
next unless c.is_a?(Class)
|
|
||||||
|
|
||||||
finders << c.new(theme)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module ThemeVersion
|
module ThemeVersion
|
||||||
@@ -28,9 +30,9 @@ module WPScan
|
|||||||
|
|
||||||
# @return [ Version ]
|
# @return [ Version ]
|
||||||
def style_version
|
def style_version
|
||||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
|
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z.-]+)/i
|
||||||
|
|
||||||
WPScan::Version.new(
|
Model::Version.new(
|
||||||
Regexp.last_match[1],
|
Regexp.last_match[1],
|
||||||
found_by: found_by,
|
found_by: found_by,
|
||||||
confidence: 80,
|
confidence: 80,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module ThemeVersion
|
module ThemeVersion
|
||||||
@@ -11,7 +13,7 @@ module WPScan
|
|||||||
|
|
||||||
return unless Regexp.last_match[1] == target.slug
|
return unless Regexp.last_match[1] == target.slug
|
||||||
|
|
||||||
WPScan::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
|
Model::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'themes/urls_in_homepage'
|
require_relative 'themes/urls_in_homepage'
|
||||||
|
require_relative 'themes/urls_in_404_page'
|
||||||
require_relative 'themes/known_locations'
|
require_relative 'themes/known_locations'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Themes
|
module Themes
|
||||||
# themes Finder
|
# Themes Finder
|
||||||
class Base
|
class Base
|
||||||
include CMSScanner::Finders::SameTypeFinder
|
include CMSScanner::Finders::SameTypeFinder
|
||||||
|
|
||||||
@@ -12,6 +15,7 @@ module WPScan
|
|||||||
def initialize(target)
|
def initialize(target)
|
||||||
finders <<
|
finders <<
|
||||||
Themes::UrlsInHomepage.new(target) <<
|
Themes::UrlsInHomepage.new(target) <<
|
||||||
|
Themes::UrlsIn404Page.new(target) <<
|
||||||
Themes::KnownLocations.new(target)
|
Themes::KnownLocations.new(target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Themes
|
module Themes
|
||||||
@@ -5,6 +7,11 @@ module WPScan
|
|||||||
class KnownLocations < CMSScanner::Finders::Finder
|
class KnownLocations < CMSScanner::Finders::Finder
|
||||||
include CMSScanner::Finders::Finder::Enumerator
|
include CMSScanner::Finders::Finder::Enumerator
|
||||||
|
|
||||||
|
# @return [ Array<Integer> ]
|
||||||
|
def valid_response_codes
|
||||||
|
@valid_response_codes ||= [200, 401, 403, 500].freeze
|
||||||
|
end
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ String ] :list
|
# @option opts [ String ] :list
|
||||||
#
|
#
|
||||||
@@ -12,12 +19,14 @@ module WPScan
|
|||||||
def aggressive(opts = {})
|
def aggressive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(target_urls(opts), opts) do |res, slug|
|
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug|
|
||||||
# TODO: follow the location (from enumerate()) and remove the 301 here ?
|
finding_opts = opts.merge(found_by: found_by,
|
||||||
# As a result, it might remove false positive due to redirection to the homepage
|
confidence: 80,
|
||||||
next unless [200, 401, 403, 301].include?(res.code)
|
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
|
||||||
|
|
||||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
found << Model::Theme.new(slug, target, finding_opts)
|
||||||
|
|
||||||
|
raise Error::ThemesThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
@@ -28,12 +37,11 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Hash ]
|
# @return [ Hash ]
|
||||||
def target_urls(opts = {})
|
def target_urls(opts = {})
|
||||||
slugs = opts[:list] || DB::Themes.vulnerable_slugs
|
slugs = opts[:list] || DB::Themes.vulnerable_slugs
|
||||||
urls = {}
|
urls = {}
|
||||||
themes_url = target.url('wp-content/themes/')
|
|
||||||
|
|
||||||
slugs.each do |slug|
|
slugs.each do |slug|
|
||||||
urls["#{themes_url}#{URI.encode(slug)}/"] = slug
|
urls[target.theme_url(slug)] = slug
|
||||||
end
|
end
|
||||||
|
|
||||||
urls
|
urls
|
||||||
|
|||||||
15
app/finders/themes/urls_in_404_page.rb
Normal file
15
app/finders/themes/urls_in_404_page.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Finders
|
||||||
|
module Themes
|
||||||
|
# URLs In 04 Page Finder
|
||||||
|
class UrlsIn404Page < UrlsInHomepage
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.error_404_res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Themes
|
module Themes
|
||||||
# URLs In Homepage Finder
|
# URLs In Homepage Finder
|
||||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||||
include WpItems::URLsInHomepage
|
include WpItems::UrlsInPage
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
@@ -12,11 +14,16 @@ module WPScan
|
|||||||
found = []
|
found = []
|
||||||
|
|
||||||
(items_from_links('themes') + items_from_codes('themes')).uniq.sort.each do |slug|
|
(items_from_links('themes') + items_from_codes('themes')).uniq.sort.each do |slug|
|
||||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.homepage_res
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'timthumb_version/bad_request'
|
require_relative 'timthumb_version/bad_request'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
@@ -7,7 +9,7 @@ module WPScan
|
|||||||
class Base
|
class Base
|
||||||
include CMSScanner::Finders::UniqueFinder
|
include CMSScanner::Finders::UniqueFinder
|
||||||
|
|
||||||
# @param [ WPScan::Timthumb ] target
|
# @param [ Model::Timthumb ] target
|
||||||
def initialize(target)
|
def initialize(target)
|
||||||
finders << TimthumbVersion::BadRequest.new(target)
|
finders << TimthumbVersion::BadRequest.new(target)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module TimthumbVersion
|
module TimthumbVersion
|
||||||
@@ -8,7 +10,7 @@ module WPScan
|
|||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
return unless Browser.get(target.url).body =~ /(TimThumb version\s*: ([^<]+))/
|
return unless Browser.get(target.url).body =~ /(TimThumb version\s*: ([^<]+))/
|
||||||
|
|
||||||
WPScan::Version.new(
|
Model::Version.new(
|
||||||
Regexp.last_match[2],
|
Regexp.last_match[2],
|
||||||
found_by: 'Bad Request (Aggressive Detection)',
|
found_by: 'Bad Request (Aggressive Detection)',
|
||||||
confidence: 90,
|
confidence: 90,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'timthumbs/known_locations'
|
require_relative 'timthumbs/known_locations'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Timthumbs
|
module Timthumbs
|
||||||
# Known Locations Timthumbs Finder
|
# Known Locations Timthumbs Finder
|
||||||
|
# Note: A vulnerable version, 2.8.13 can be found here:
|
||||||
|
# https://github.com/GabrielGil/TimThumb/blob/980c3d6a823477761570475e8b83d3e9fcd2d7ae/timthumb.php
|
||||||
class KnownLocations < CMSScanner::Finders::Finder
|
class KnownLocations < CMSScanner::Finders::Finder
|
||||||
include CMSScanner::Finders::Finder::Enumerator
|
include CMSScanner::Finders::Finder::Enumerator
|
||||||
|
|
||||||
|
# @return [ Array<Integer> ]
|
||||||
|
def valid_response_codes
|
||||||
|
@valid_response_codes ||= [400]
|
||||||
|
end
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ String ] :list Mandatory
|
# @option opts [ String ] :list Mandatory
|
||||||
#
|
#
|
||||||
@@ -12,10 +21,10 @@ module WPScan
|
|||||||
def aggressive(opts = {})
|
def aggressive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(target_urls(opts), opts) do |res|
|
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
|
||||||
next unless res.code == 400 && res.body =~ /no image specified/i
|
next unless /no image specified/i.match?(res.body)
|
||||||
|
|
||||||
found << WPScan::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
|
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'users/author_posts'
|
require_relative 'users/author_posts'
|
||||||
require_relative 'users/wp_json_api'
|
require_relative 'users/wp_json_api'
|
||||||
require_relative 'users/oembed_api'
|
require_relative 'users/oembed_api'
|
||||||
require_relative 'users/rss_generator'
|
require_relative 'users/rss_generator'
|
||||||
require_relative 'users/author_id_brute_forcing'
|
require_relative 'users/author_id_brute_forcing'
|
||||||
require_relative 'users/login_error_messages'
|
require_relative 'users/login_error_messages'
|
||||||
require_relative 'users/yoast_seo_author_sitemap.rb'
|
require_relative 'users/author_sitemap'
|
||||||
|
require_relative 'users/yoast_seo_author_sitemap'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
|
# Specific Finders container to filter the usernames found
|
||||||
|
# and remove the ones matching ParsedCli.exclude_username if supplied
|
||||||
|
class UsersFinders < SameTypeFinders
|
||||||
|
def filter_findings
|
||||||
|
findings.delete_if { |user| ParsedCli.exclude_usernames.match?(user.username) } if ParsedCli.exclude_usernames
|
||||||
|
|
||||||
|
findings
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
module Users
|
module Users
|
||||||
# Users Finder
|
# Users Finder
|
||||||
class Base
|
class Base
|
||||||
@@ -20,10 +33,15 @@ module WPScan
|
|||||||
Users::WpJsonApi.new(target) <<
|
Users::WpJsonApi.new(target) <<
|
||||||
Users::OembedApi.new(target) <<
|
Users::OembedApi.new(target) <<
|
||||||
Users::RSSGenerator.new(target) <<
|
Users::RSSGenerator.new(target) <<
|
||||||
|
Users::AuthorSitemap.new(target) <<
|
||||||
Users::YoastSeoAuthorSitemap.new(target) <<
|
Users::YoastSeoAuthorSitemap.new(target) <<
|
||||||
Users::AuthorIdBruteForcing.new(target) <<
|
Users::AuthorIdBruteForcing.new(target) <<
|
||||||
Users::LoginErrorMessages.new(target)
|
Users::LoginErrorMessages.new(target)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def finders
|
||||||
|
@finders ||= Finders::UsersFinders.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Users
|
module Users
|
||||||
@@ -5,6 +7,11 @@ module WPScan
|
|||||||
class AuthorIdBruteForcing < CMSScanner::Finders::Finder
|
class AuthorIdBruteForcing < CMSScanner::Finders::Finder
|
||||||
include CMSScanner::Finders::Finder::Enumerator
|
include CMSScanner::Finders::Finder::Enumerator
|
||||||
|
|
||||||
|
# @return [ Array<Integer> ]
|
||||||
|
def valid_response_codes
|
||||||
|
@valid_response_codes ||= [200, 301, 302]
|
||||||
|
end
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ Range ] :range Mandatory
|
# @option opts [ Range ] :range Mandatory
|
||||||
#
|
#
|
||||||
@@ -13,12 +20,12 @@ module WPScan
|
|||||||
found = []
|
found = []
|
||||||
found_by_msg = 'Author Id Brute Forcing - %s (Aggressive Detection)'
|
found_by_msg = 'Author Id Brute Forcing - %s (Aggressive Detection)'
|
||||||
|
|
||||||
enumerate(target_urls(opts), opts) do |res, id|
|
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, id|
|
||||||
username, found_by, confidence = potential_username(res)
|
username, found_by, confidence = potential_username(res)
|
||||||
|
|
||||||
next unless username
|
next unless username
|
||||||
|
|
||||||
found << CMSScanner::User.new(
|
found << Model::User.new(
|
||||||
username,
|
username,
|
||||||
id: id,
|
id: id,
|
||||||
found_by: format(found_by_msg, found_by),
|
found_by: format(found_by_msg, found_by),
|
||||||
@@ -47,7 +54,7 @@ module WPScan
|
|||||||
super(opts.merge(title: ' Brute Forcing Author IDs -'))
|
super(opts.merge(title: ' Brute Forcing Author IDs -'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_params
|
def full_request_params
|
||||||
{ followlocation: true }
|
{ followlocation: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,11 +71,13 @@ module WPScan
|
|||||||
return username, 'Display Name', 50 if username
|
return username, 'Display Name', 50 if username
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] url
|
# @param [ String, Addressable::URI ] uri
|
||||||
#
|
#
|
||||||
# @return [ String, nil ]
|
# @return [ String, nil ]
|
||||||
def username_from_author_url(url)
|
def username_from_author_url(uri)
|
||||||
url[%r{/author/([^/\b]+)/?}i, 1]
|
uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
|
||||||
|
|
||||||
|
uri.path[%r{/author/([^/\b]+)/?}i, 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ Typhoeus::Response ] res
|
# @param [ Typhoeus::Response ] res
|
||||||
@@ -76,12 +85,12 @@ module WPScan
|
|||||||
# @return [ String, nil ] The username found
|
# @return [ String, nil ] The username found
|
||||||
def username_from_response(res)
|
def username_from_response(res)
|
||||||
# Permalink enabled
|
# Permalink enabled
|
||||||
target.in_scope_urls(res, '//link/@href|//a/@href') do |url|
|
target.in_scope_uris(res, '//@href[contains(., "author/")]') do |uri|
|
||||||
username = username_from_author_url(url)
|
username = username_from_author_url(uri)
|
||||||
return username if username
|
return username if username
|
||||||
end
|
end
|
||||||
|
|
||||||
# No permalink
|
# No permalink, TODO Maybe use xpath to extract the classes ?
|
||||||
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -90,9 +99,12 @@ module WPScan
|
|||||||
# @return [ String, nil ]
|
# @return [ String, nil ]
|
||||||
def display_name_from_body(body)
|
def display_name_from_body(body)
|
||||||
page = Nokogiri::HTML.parse(body)
|
page = Nokogiri::HTML.parse(body)
|
||||||
|
|
||||||
# WP >= 3.0
|
# WP >= 3.0
|
||||||
page.css('h1.page-title span').each do |node|
|
page.css('h1.page-title span').each do |node|
|
||||||
return node.text.to_s
|
text = node.text.to_s.strip
|
||||||
|
|
||||||
|
return text unless text.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
# WP < 3.0
|
# WP < 3.0
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Users
|
module Users
|
||||||
@@ -10,7 +12,7 @@ module WPScan
|
|||||||
found_by_msg = 'Author Posts - %s (Passive Detection)'
|
found_by_msg = 'Author Posts - %s (Passive Detection)'
|
||||||
|
|
||||||
usernames(opts).reduce([]) do |a, e|
|
usernames(opts).reduce([]) do |a, e|
|
||||||
a << CMSScanner::User.new(
|
a << Model::User.new(
|
||||||
e[0],
|
e[0],
|
||||||
found_by: format(found_by_msg, e[1]),
|
found_by: format(found_by_msg, e[1]),
|
||||||
confidence: e[2]
|
confidence: e[2]
|
||||||
@@ -43,12 +45,10 @@ module WPScan
|
|||||||
def potential_usernames(res)
|
def potential_usernames(res)
|
||||||
usernames = []
|
usernames = []
|
||||||
|
|
||||||
target.in_scope_urls(res, '//a/@href') do |url, node|
|
target.in_scope_uris(res, '//a/@href[contains(., "author")]') do |uri, node|
|
||||||
uri = Addressable::URI.parse(url)
|
|
||||||
|
|
||||||
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
|
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
|
||||||
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
||||||
elsif uri.query =~ /author=[0-9]+/
|
elsif /author=[0-9]+/.match?(uri.query)
|
||||||
usernames << [node.text.to_s.strip, 'Display Name', 30]
|
usernames << [node.text.to_s.strip, 'Display Name', 30]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
36
app/finders/users/author_sitemap.rb
Normal file
36
app/finders/users/author_sitemap.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Finders
|
||||||
|
module Users
|
||||||
|
# Since WP 5.5, /wp-sitemap-users-1.xml is generated and contains
|
||||||
|
# the usernames of accounts who made a post
|
||||||
|
class AuthorSitemap < CMSScanner::Finders::Finder
|
||||||
|
# @param [ Hash ] opts
|
||||||
|
#
|
||||||
|
# @return [ Array<User> ]
|
||||||
|
def aggressive(_opts = {})
|
||||||
|
found = []
|
||||||
|
|
||||||
|
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
|
||||||
|
username = user_tag.text.to_s[%r{/author/([^/]+)/}, 1]
|
||||||
|
|
||||||
|
next unless username && !username.strip.empty?
|
||||||
|
|
||||||
|
found << Model::User.new(username,
|
||||||
|
found_by: found_by,
|
||||||
|
confidence: 100,
|
||||||
|
interesting_entries: [sitemap_url])
|
||||||
|
end
|
||||||
|
|
||||||
|
found
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ String ] The URL of the sitemap
|
||||||
|
def sitemap_url
|
||||||
|
@sitemap_url ||= target.url('wp-sitemap-users-1.xml')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Users
|
module Users
|
||||||
@@ -22,9 +24,9 @@ module WPScan
|
|||||||
|
|
||||||
return found if error.empty? # Protection plugin / error disabled
|
return found if error.empty? # Protection plugin / error disabled
|
||||||
|
|
||||||
next unless error =~ /The password you entered for the username|Incorrect Password/i
|
next unless /The password you entered for the username|Incorrect Password/i.match?(error)
|
||||||
|
|
||||||
found << CMSScanner::User.new(username, found_by: found_by, confidence: 100)
|
found << Model::User.new(username, found_by: found_by, confidence: 100)
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
@@ -35,7 +37,7 @@ module WPScan
|
|||||||
# usernames from the potential Users found
|
# usernames from the potential Users found
|
||||||
unames = opts[:found].map(&:username)
|
unames = opts[:found].map(&:username)
|
||||||
|
|
||||||
[*opts[:list]].each { |uname| unames << uname.chomp }
|
Array(opts[:list]).each { |uname| unames << uname.chomp }
|
||||||
|
|
||||||
unames.uniq
|
unames.uniq
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Users
|
module Users
|
||||||
@@ -21,10 +23,10 @@ module WPScan
|
|||||||
|
|
||||||
return [] unless details
|
return [] unless details
|
||||||
|
|
||||||
[CMSScanner::User.new(details[0],
|
[Model::User.new(details[0],
|
||||||
found_by: format(found_by_msg, details[1]),
|
found_by: format(found_by_msg, details[1]),
|
||||||
confidence: details[2],
|
confidence: details[2],
|
||||||
interesting_entries: [api_url])]
|
interesting_entries: [api_url])]
|
||||||
rescue JSON::ParserError
|
rescue JSON::ParserError
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
@@ -32,6 +34,8 @@ module WPScan
|
|||||||
def user_details_from_oembed_data(oembed_data)
|
def user_details_from_oembed_data(oembed_data)
|
||||||
return unless oembed_data
|
return unless oembed_data
|
||||||
|
|
||||||
|
oembed_data = oembed_data.first if oembed_data.is_a?(Array)
|
||||||
|
|
||||||
if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z}
|
if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z}
|
||||||
details = [Regexp.last_match[1], 'Author URL', 90]
|
details = [Regexp.last_match[1], 'Author URL', 90]
|
||||||
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?
|
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?
|
||||||
|
|||||||
@@ -1,36 +1,38 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Users
|
module Users
|
||||||
# Users disclosed from the dc:creator field in the RSS
|
# Users disclosed from the dc:creator field in the RSS
|
||||||
# The names disclosed are display names, however depending on the configuration of the blog,
|
# The names disclosed are display names, however depending on the configuration of the blog,
|
||||||
# they can be the same than usernames
|
# they can be the same than usernames
|
||||||
class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
|
class RSSGenerator < Finders::WpVersion::RSSGenerator
|
||||||
def process_urls(urls, _opts = {})
|
def process_urls(urls, _opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
urls.each do |url|
|
urls.each do |url|
|
||||||
res = Browser.get_and_follow_location(url)
|
res = Browser.get_and_follow_location(url)
|
||||||
|
|
||||||
next unless res.code == 200 && res.body =~ /<dc\:creator>/i
|
next unless res.code == 200 && res.body =~ /<dc:creator>/i
|
||||||
|
|
||||||
potential_usernames = []
|
potential_usernames = []
|
||||||
|
|
||||||
begin
|
begin
|
||||||
res.xml.xpath('//item/dc:creator').each do |node|
|
res.xml.xpath('//item/dc:creator').each do |node|
|
||||||
potential_username = node.text.to_s
|
username = node.text.to_s
|
||||||
|
|
||||||
# Ignoring potential username longer than 60 characters and containing accents
|
# Ignoring potential username longer than 60 characters and containing accents
|
||||||
# as they are considered invalid. See https://github.com/wpscanteam/wpscan/issues/1215
|
# as they are considered invalid. See https://github.com/wpscanteam/wpscan/issues/1215
|
||||||
next if potential_username.length > 60 || potential_username =~ /[^\x00-\x7F]/
|
next if username.strip.empty? || username.length > 60 || username =~ /[^\x00-\x7F]/
|
||||||
|
|
||||||
potential_usernames << potential_username
|
potential_usernames << username
|
||||||
end
|
end
|
||||||
rescue Nokogiri::XML::XPath::SyntaxError
|
rescue Nokogiri::XML::XPath::SyntaxError
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
potential_usernames.uniq.each do |potential_username|
|
potential_usernames.uniq.each do |username|
|
||||||
found << CMSScanner::User.new(potential_username, found_by: found_by, confidence: 50)
|
found << Model::User.new(username, found_by: found_by, confidence: 50)
|
||||||
end
|
end
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Users
|
module Users
|
||||||
# WP JSON API
|
# WP JSON API
|
||||||
#
|
#
|
||||||
# Since 4.7 - Need more investigation as it seems WP 4.7.1 reduces the exposure, see https://github.com/wpscanteam/wpscan/issues/1038)
|
# Since 4.7 - Need more investigation as it seems WP 4.7.1 reduces the exposure, see https://github.com/wpscanteam/wpscan/issues/1038)
|
||||||
|
# For the pagination, see https://github.com/wpscanteam/wpscan/issues/1285
|
||||||
#
|
#
|
||||||
class WpJsonApi < CMSScanner::Finders::Finder
|
class WpJsonApi < CMSScanner::Finders::Finder
|
||||||
|
MAX_PER_PAGE = 100 # See https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
# @return [ Array<User> ]
|
# @return [ Array<User> ]
|
||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
current_page = 0
|
||||||
|
|
||||||
JSON.parse(Browser.get(api_url).body)&.each do |user|
|
loop do
|
||||||
found << CMSScanner::User.new(user['slug'],
|
current_page += 1
|
||||||
id: user['id'],
|
|
||||||
found_by: found_by,
|
res = Browser.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
|
||||||
confidence: 100,
|
|
||||||
interesting_entries: [api_url])
|
total_pages ||= res.headers['X-WP-TotalPages'].to_i
|
||||||
|
|
||||||
|
users_in_page = users_from_response(res)
|
||||||
|
found += users_in_page
|
||||||
|
|
||||||
|
break if current_page >= total_pages || users_in_page.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
found
|
found
|
||||||
@@ -25,9 +36,32 @@ module WPScan
|
|||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param [ Typhoeus::Response ] response
|
||||||
|
#
|
||||||
|
# @return [ Array<User> ] The users from the response
|
||||||
|
def users_from_response(response)
|
||||||
|
found = []
|
||||||
|
|
||||||
|
JSON.parse(response.body)&.each do |user|
|
||||||
|
found << Model::User.new(user['slug'],
|
||||||
|
id: user['id'],
|
||||||
|
found_by: found_by,
|
||||||
|
confidence: 100,
|
||||||
|
interesting_entries: [response.effective_url])
|
||||||
|
end
|
||||||
|
|
||||||
|
found
|
||||||
|
end
|
||||||
|
|
||||||
# @return [ String ] The URL of the API listing the Users
|
# @return [ String ] The URL of the API listing the Users
|
||||||
def api_url
|
def api_url
|
||||||
@api_url ||= target.url('wp-json/wp/v2/users/')
|
return @api_url if @api_url
|
||||||
|
|
||||||
|
target.in_scope_uris(target.homepage_res, "//link[@rel='https://api.w.org/']/@href").each do |uri|
|
||||||
|
return @api_url = uri.join('wp/v2/users/').to_s if uri.path.include?('wp-json')
|
||||||
|
end
|
||||||
|
|
||||||
|
@api_url = target.url('wp-json/wp/v2/users/')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Users
|
module Users
|
||||||
# The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
|
# The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
|
||||||
# See https://github.com/wpscanteam/wpscan/issues/1228
|
# See https://github.com/wpscanteam/wpscan/issues/1228
|
||||||
class YoastSeoAuthorSitemap < CMSScanner::Finders::Finder
|
class YoastSeoAuthorSitemap < AuthorSitemap
|
||||||
# @param [ Hash ] opts
|
|
||||||
#
|
|
||||||
# @return [ Array<User> ]
|
|
||||||
def aggressive(_opts = {})
|
|
||||||
found = []
|
|
||||||
|
|
||||||
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
|
|
||||||
username = user_tag.text.to_s[%r{/author/([^\/]+)/}, 1]
|
|
||||||
|
|
||||||
next unless username && !username.strip.empty?
|
|
||||||
|
|
||||||
found << CMSScanner::User.new(username,
|
|
||||||
found_by: found_by,
|
|
||||||
confidence: 100,
|
|
||||||
interesting_entries: [sitemap_url])
|
|
||||||
end
|
|
||||||
|
|
||||||
found
|
|
||||||
end
|
|
||||||
|
|
||||||
# @return [ String ] The URL of the author-sitemap
|
# @return [ String ] The URL of the author-sitemap
|
||||||
def sitemap_url
|
def sitemap_url
|
||||||
@sitemap_url ||= target.url('author-sitemap.xml')
|
@sitemap_url ||= target.url('author-sitemap.xml')
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
require_relative 'wp_items/urls_in_homepage'
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'wp_items/urls_in_page'
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module WpItems
|
module WpItems
|
||||||
# URLs In Homepage Module to use in plugins & themes finders
|
# URLs In Homepage Module to use in plugins & themes finders
|
||||||
module URLsInHomepage
|
module UrlsInPage
|
||||||
# @param [ String ] type plugins / themes
|
# @param [ String ] type plugins / themes
|
||||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||||
#
|
#
|
||||||
# @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
|
# @return [ Array<String> ] The plugins/themes detected in the href, src attributes of the page
|
||||||
def items_from_links(type, uniq = true)
|
def items_from_links(type, uniq: true)
|
||||||
found = []
|
found = []
|
||||||
|
xpath = format(
|
||||||
|
'(//@href|//@src|//@data-src)[contains(., "%s")]',
|
||||||
|
type == 'plugins' ? target.plugins_dir : target.content_dir
|
||||||
|
)
|
||||||
|
|
||||||
target.in_scope_urls(target.homepage_res) do |url|
|
target.in_scope_uris(page_res, xpath) do |uri|
|
||||||
next unless url =~ item_attribute_pattern(type)
|
next unless uri.to_s =~ item_attribute_pattern(type)
|
||||||
|
|
||||||
found << Regexp.last_match[1]
|
slug = Regexp.last_match[1]&.strip
|
||||||
|
|
||||||
|
found << slug unless slug&.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
uniq ? found.uniq.sort : found.sort
|
uniq ? found.uniq.sort : found.sort
|
||||||
@@ -23,10 +31,10 @@ module WPScan
|
|||||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||||
#
|
#
|
||||||
# @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
|
# @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
|
||||||
def items_from_codes(type, uniq = true)
|
def items_from_codes(type, uniq: true)
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
target.homepage_res.html.css('script,style').each do |tag|
|
page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
|
||||||
code = tag.text.to_s
|
code = tag.text.to_s
|
||||||
next if code.empty?
|
next if code.empty?
|
||||||
|
|
||||||
@@ -40,14 +48,14 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Regexp ]
|
# @return [ Regexp ]
|
||||||
def item_attribute_pattern(type)
|
def item_attribute_pattern(type)
|
||||||
@item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
|
@item_attribute_pattern ||= %r{#{item_url_pattern(type)}([^/]+)/}i
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] type
|
# @param [ String ] type
|
||||||
#
|
#
|
||||||
# @return [ Regexp ]
|
# @return [ Regexp ]
|
||||||
def item_code_pattern(type)
|
def item_code_pattern(type)
|
||||||
@item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
|
@item_code_pattern ||= %r{["'( ]#{item_url_pattern(type)}([^\\/)"']+)}i
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] type
|
# @param [ String ] type
|
||||||
@@ -57,10 +65,10 @@ module WPScan
|
|||||||
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
|
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
|
||||||
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
||||||
|
|
||||||
url = /#{item_url.gsub(/\A(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||||
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
item_dir = %r{(?:#{url}|\\?/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||||
|
|
||||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
|
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?/}i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'wp_version/rss_generator'
|
require_relative 'wp_version/rss_generator'
|
||||||
require_relative 'wp_version/atom_generator'
|
require_relative 'wp_version/atom_generator'
|
||||||
require_relative 'wp_version/rdf_generator'
|
require_relative 'wp_version/rdf_generator'
|
||||||
@@ -8,7 +10,7 @@ module WPScan
|
|||||||
module Finders
|
module Finders
|
||||||
# Specific Finders container to filter the version detected
|
# Specific Finders container to filter the version detected
|
||||||
# and remove the one with low confidence to avoid false
|
# and remove the one with low confidence to avoid false
|
||||||
# positive when there is not enought information to accurately
|
# positive when there is not enough information to accurately
|
||||||
# determine it.
|
# determine it.
|
||||||
class WpVersionFinders < UniqueFinders
|
class WpVersionFinders < UniqueFinders
|
||||||
def filter_findings
|
def filter_findings
|
||||||
@@ -26,7 +28,7 @@ module WPScan
|
|||||||
# @param [ WPScan::Target ] target
|
# @param [ WPScan::Target ] target
|
||||||
def initialize(target)
|
def initialize(target)
|
||||||
(%w[RSSGenerator AtomGenerator RDFGenerator] +
|
(%w[RSSGenerator AtomGenerator RDFGenerator] +
|
||||||
WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
|
DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
|
||||||
%w[Readme UniqueFingerprinting]
|
%w[Readme UniqueFingerprinting]
|
||||||
).each do |finder_name|
|
).each do |finder_name|
|
||||||
finders << WpVersion.const_get(finder_name.to_sym).new(target)
|
finders << WpVersion.const_get(finder_name.to_sym).new(target)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user