Skip to content

Commit 48e05ca

Browse files
authored
Merge pull request projectdiscovery#7277 from HarshadaGawas05/feat/honeypot-detection
feat: add honeypot detection to reduce scan noise (projectdiscovery#6403)
2 parents d5eafeb + 3bcf21d commit 48e05ca

File tree

13 files changed

+436
-21
lines changed

13 files changed

+436
-21
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ UPDATE:
338338
-ud, -update-template-dir string custom directory to install / update nuclei-templates
339339
-duc, -disable-update-check disable automatic nuclei/templates update check
340340

341+
HONEYPOT:
342+
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
343+
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
344+
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
345+
341346
STATISTICS:
342347
-stats display statistics about the running scan
343348
-sj, -stats-json display statistics in JSONL(ines) format

README_CN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ UNCOVER引擎:
291291
-ud, -update-template-dir string 指定模板目录
292292
-duc, -disable-update-check 禁用nuclei程序与模板更新
293293

294+
HONEYPOT:
295+
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
296+
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
297+
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
298+
294299
统计:
295300
-stats 显示正在扫描的统计信息
296301
-sj, -stats-json 将统计信息以JSONL格式输出到文件

README_ES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ UPDATE:
296296
-ud, -update-template-dir string directorio personalizado para instalar/actualizar nuclei-templates
297297
-duc, -disable-update-check deshabilita la comprobación automática de actualizaciones de nuclei/templates
298298

299+
HONEYPOT:
300+
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
301+
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
302+
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
303+
299304
STATISTICS:
300305
-stats muestra estadísticas sobre el escaneo en ejecución
301306
-sj, -stats-json muestra estadísticas en formato JSONL(ines)

README_ID.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,11 @@ UPDATE:
262262
-ud, -update-template-dir string custom directory to install / update nuclei-templates
263263
-duc, -disable-update-check disable automatic nuclei/templates update check
264264

265+
HONEYPOT:
266+
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
267+
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
268+
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
269+
265270
STATISTICS:
266271
-stats display statistics about the running scan
267272
-sj, -stats-json display statistics in JSONL(ines) format

README_KR.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ UPDATE:
261261
-ud, -update-template-dir string nuclei-templates를 설치/업데이트할 사용자 지정 디렉토리
262262
-duc, -disable-update-check 자동 nuclei/templates 업데이트 확인 비활성화
263263

264+
HONEYPOT:
265+
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
266+
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
267+
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
268+
264269
STATISTICS:
265270
-stats 실행 중인 스캔에 대한 통계 표시
266271
-sj, -stats-json JSONL(ines) 형식으로 통계 표시

README_PT-BR.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ UPDATE:
296296
-ud, -update-template-dir string diretório personalizado para instalar/atualizar os nuclei-templates
297297
-duc, -disable-update-check desativa a verificação automática de atualizações do nuclei/templates
298298

299+
HONEYPOT:
300+
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
301+
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
302+
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
303+
299304
STATISTICS:
300305
-stats exibe estatísticas sobre o scan em execução
301306
-sj, -stats-json exibe estatísticas no formato JSONL(ines)

cmd/nuclei/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,12 @@ on extensive configurability, massive extensibility and ease of use.`)
487487
flagSet.CallbackVarP(disableUpdatesCallback, "disable-update-check", "duc", "disable automatic nuclei/templates update check"),
488488
)
489489

490+
flagSet.CreateGroup("Honeypot", "Honeypot",
491+
flagSet.BoolVarP(&options.HoneypotDetection, "honeypot-detect", "hpd", false, "detect potential honeypot hosts based on match concentration"),
492+
flagSet.IntVarP(&options.HoneypotThreshold, "honeypot-threshold", "hpt", 15, "number of distinct template IDs required to flag a honeypot host"),
493+
flagSet.BoolVarP(&options.SuppressHoneypotResults, "suppress-honeypot", "shp", false, "suppress output for flagged honeypot hosts"),
494+
)
495+
490496
flagSet.CreateGroup("stats", "Statistics",
491497
flagSet.BoolVar(&options.EnableProgressBar, "stats", false, "display statistics about the running scan"),
492498
flagSet.BoolVarP(&options.StatsJSON, "stats-json", "sj", false, "display statistics in JSONL(ines) format"),

internal/runner/runner.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import (
5454
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
5555
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
5656
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
57+
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
5758
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/uncover"
5859
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
5960
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
@@ -97,6 +98,8 @@ type Runner struct {
9798
httpStats *outputstats.Tracker
9899
Logger *gologger.Logger
99100

101+
honeypotDetector *honeypotdetector.Detector
102+
100103
//general purpose temporary directory
101104
tmpDir string
102105
parser parser.Parser
@@ -261,6 +264,12 @@ func New(options *types.Options) (*Runner, error) {
261264
}
262265
}()
263266

267+
// Initialize honeypot detector (opt-in) so results can be suppressed.
268+
var hpDetector *honeypotdetector.Detector
269+
if options.HoneypotDetection {
270+
hpDetector = honeypotdetector.New(options.HoneypotThreshold)
271+
}
272+
264273
// create the input provider and load the inputs
265274
inputProvider, err := provider.NewInputProvider(provider.InputOptions{Options: options, TempDir: runner.tmpDir})
266275
if err != nil {
@@ -273,6 +282,10 @@ func New(options *types.Options) (*Runner, error) {
273282
if err != nil {
274283
return nil, errors.Wrap(err, "could not create output file")
275284
}
285+
if hpDetector != nil {
286+
outputWriter.SetHoneypotDetector(hpDetector)
287+
runner.honeypotDetector = hpDetector
288+
}
276289
// setup a proxy writer to automatically upload results to PDCP
277290
runner.output = runner.setupPDCPUpload(outputWriter)
278291
if options.HTTPStats {
@@ -421,6 +434,10 @@ func (r *Runner) Close() {
421434
if r.output != nil {
422435
r.output.Close()
423436
}
437+
438+
if r.honeypotDetector != nil {
439+
r.Logger.Print().Msgf("%s\n", r.honeypotDetector.Summary())
440+
}
424441
if r.issuesClient != nil {
425442
r.issuesClient.Close()
426443
}

pkg/output/output.go

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package output
22

33
import (
44
"encoding/base64"
5+
stderrors "errors"
56
"fmt"
67
"io"
78
"log/slog"
89
"maps"
10+
"net"
911
"os"
1012
"path/filepath"
1113
"regexp"
@@ -27,6 +29,7 @@ import (
2729
"github.com/projectdiscovery/nuclei/v3/pkg/model"
2830
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
2931
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
32+
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
3033
protocolUtils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
3134
"github.com/projectdiscovery/nuclei/v3/pkg/types"
3235
"github.com/projectdiscovery/nuclei/v3/pkg/types/nucleierr"
@@ -38,6 +41,10 @@ import (
3841
urlutil "github.com/projectdiscovery/utils/url"
3942
)
4043

44+
// ErrHoneypotSuppressed is returned by the output writer when a match result is suppressed
45+
// due to honeypot detection.
46+
var ErrHoneypotSuppressed = stderrors.New("honeypot suppressed result")
47+
4148
// Writer is an interface which writes output to somewhere for nuclei events.
4249
type Writer interface {
4350
// Close closes the output writer interface
@@ -65,6 +72,9 @@ type StandardWriter struct {
6572
timestamp bool
6673
noMetadata bool
6774
matcherStatus bool
75+
honeypotDetector *honeypotdetector.Detector
76+
suppressHoneypot bool
77+
honeypotThreshold int
6878
mutex *sync.Mutex
6979
aurora aurora.Aurora
7080
outputFile io.WriteCloser
@@ -265,21 +275,23 @@ func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
265275
}
266276

267277
writer := &StandardWriter{
268-
json: options.JSONL,
269-
jsonReqResp: !options.OmitRawRequests,
270-
noMetadata: options.NoMeta,
271-
matcherStatus: options.MatcherStatus,
272-
timestamp: options.Timestamp,
273-
aurora: auroraColorizer,
274-
mutex: &sync.Mutex{},
275-
outputFile: outputFile,
276-
traceFile: traceOutput,
277-
errorFile: errorOutput,
278-
severityColors: colorizer.New(auroraColorizer),
279-
storeResponse: options.StoreResponse,
280-
storeResponseDir: options.StoreResponseDir,
281-
omitTemplate: options.OmitTemplate,
282-
KeysToRedact: options.Redact,
278+
json: options.JSONL,
279+
jsonReqResp: !options.OmitRawRequests,
280+
noMetadata: options.NoMeta,
281+
matcherStatus: options.MatcherStatus,
282+
timestamp: options.Timestamp,
283+
suppressHoneypot: options.SuppressHoneypotResults,
284+
honeypotThreshold: options.HoneypotThreshold,
285+
aurora: auroraColorizer,
286+
mutex: &sync.Mutex{},
287+
outputFile: outputFile,
288+
traceFile: traceOutput,
289+
errorFile: errorOutput,
290+
severityColors: colorizer.New(auroraColorizer),
291+
storeResponse: options.StoreResponse,
292+
storeResponseDir: options.StoreResponseDir,
293+
omitTemplate: options.OmitTemplate,
294+
KeysToRedact: options.Redact,
283295
}
284296

285297
if v := os.Getenv("DISABLE_STDOUT"); v == "true" || v == "1" {
@@ -289,6 +301,14 @@ func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
289301
return writer, nil
290302
}
291303

304+
// SetHoneypotDetector attaches an initialized honeypot detector to the writer.
305+
func (w *StandardWriter) SetHoneypotDetector(detector *honeypotdetector.Detector) {
306+
w.honeypotDetector = detector
307+
if detector != nil {
308+
w.honeypotThreshold = detector.Threshold()
309+
}
310+
}
311+
292312
func (w *StandardWriter) ResultCount() int {
293313
return int(w.resultCount.Load())
294314
}
@@ -299,6 +319,29 @@ func (w *StandardWriter) Write(event *ResultEvent) error {
299319
return nil
300320
}
301321

322+
// Honeypot detection is performed only for successful matches.
323+
if event.MatcherStatus && w.honeypotDetector != nil {
324+
hostKey := event.URL
325+
if hostKey == "" && event.Host != "" {
326+
hostKey = event.Host
327+
if event.Port != "" {
328+
hostKey = net.JoinHostPort(event.Host, event.Port)
329+
}
330+
}
331+
332+
if hostKey != "" {
333+
justFlagged := w.honeypotDetector.RecordMatch(hostKey, event.TemplateID)
334+
if justFlagged {
335+
normalized := honeypotdetector.NormalizeHostKey(hostKey)
336+
gologger.Warning().Msgf("Potential honeypot detected: %s (matched %d distinct templates)", normalized, w.honeypotThreshold)
337+
}
338+
339+
if w.suppressHoneypot && w.honeypotDetector.IsFlagged(hostKey) {
340+
return ErrHoneypotSuppressed
341+
}
342+
}
343+
}
344+
302345
// Enrich the result event with extra metadata on the template-path and url.
303346
if event.TemplatePath != "" {
304347
event.Template, event.TemplateURL = utils.TemplatePathURL(types.ToString(event.TemplatePath), types.ToString(event.TemplateID), event.TemplateVerifier)
Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package writer
22

33
import (
4+
stderrors "errors"
5+
46
"github.com/projectdiscovery/gologger"
57
"github.com/projectdiscovery/nuclei/v3/pkg/output"
68
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
79
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
810
)
911

1012
// WriteResult is a helper for writing results to the output
11-
func WriteResult(data *output.InternalWrappedEvent, output output.Writer, progress progress.Progress, issuesClient reporting.Client) bool {
13+
func WriteResult(data *output.InternalWrappedEvent, out output.Writer, progress progress.Progress, issuesClient reporting.Client) bool {
1214
// Handle the case where no result found for the template.
1315
// In this case, we just show misc information about the failed
1416
// match for the template.
@@ -17,18 +19,27 @@ func WriteResult(data *output.InternalWrappedEvent, output output.Writer, progre
1719
}
1820
var matched bool
1921
for _, result := range data.Results {
20-
if issuesClient != nil {
22+
var suppressed bool
23+
if err := out.Write(result); err != nil {
24+
if stderrors.Is(err, output.ErrHoneypotSuppressed) {
25+
suppressed = true
26+
} else {
27+
gologger.Warning().Msgf("Could not write output event: %s\n", err)
28+
}
29+
}
30+
31+
// Only create issues when the result was not suppressed.
32+
if issuesClient != nil && !suppressed {
2133
if err := issuesClient.CreateIssue(result); err != nil {
2234
gologger.Warning().Msgf("Could not create issue on tracker: %s", err)
2335
}
2436
}
25-
if err := output.Write(result); err != nil {
26-
gologger.Warning().Msgf("Could not write output event: %s\n", err)
27-
}
2837
if !matched {
2938
matched = true
3039
}
31-
progress.IncrementMatched()
40+
if !suppressed {
41+
progress.IncrementMatched()
42+
}
3243
}
3344
return matched
3445
}

0 commit comments

Comments
 (0)