← Back to writing
Tools & Cheatsheets

Writing Custom Nuclei Templates

Jul 29, 2024
4 min read
lawbyte

Nuclei is powerful out of the box, but the real value comes from writing custom templates for application-specific vulnerabilities. This guide covers the full template syntax so you can automate detection of any vulnerability you discover during pentests.

Template Anatomy

id: template-name                    # unique identifier, lowercase-kebab

info:
name: Human Readable Name # display name
author: yourhandle # your handle
severity: critical # info | low | medium | high | critical
description: |
What this template detects and why it matters.
reference:
- https://cve.mitre.org/cve/CVE-2024-XXXX
tags: cve,rce,apache # for filtering with -tags

requests: # or dns, network, headless, ssl, http
- method: GET
path:
- "{{BaseURL}}/endpoint"
matchers:
- type: status
status:
- 200

HTTP Request Basics

requests:
- method: POST
path:
- "{{BaseURL}}/api/login"
headers:
Content-Type: application/json
Accept: application/json
body: '{"username":"admin","password":"admin"}'

matchers-condition: and # all matchers must match (default)
matchers:
- type: status
status:
- 200
- type: word
words:
- '"role":"admin"'
- '"authenticated":true'
condition: or # any of these words

Matcher Types

Status

matchers:
- type: status
status:
- 200
- 201

Word

matchers:
- type: word
part: body # body (default), header, all
words:
- "root:x:0:0" # /etc/passwd content
- "[MySQL]" # database error
condition: or # or | and
negative: false # set true to invert match

Regex

matchers:
- type: regex
part: body
regex:
- 'uid=\d+\(.*\)' # Linux id command output
- 'AWS_ACCESS_KEY_ID=AKIA[0-9A-Z]{16}'

DSL (Dynamic conditions)

matchers:
- type: dsl
dsl:
- 'status_code == 200 && contains(body, "error")'
- 'len(body) > 1000'
- 'contains(header, "X-Powered-By: PHP")'

Size

matchers:
- type: size
size:
- 1337
part: body

Binary

matchers:
- type: binary
binary:
- "504b0304" # ZIP magic bytes hex
part: body

Multiple Requests (Multi-Step)

Chain requests together — extract values from one and use in the next:

requests:
# Step 1 — get CSRF token
- method: GET
path:
- "{{BaseURL}}/login"
extractors:
- type: regex
name: csrf_token
regex:
- 'name="csrf_token" value="([^"]+)"'
group: 1 # capture group 1

# Step 2 — use extracted token
- method: POST
path:
- "{{BaseURL}}/login"
body: "username=admin&password=admin&csrf_token={{csrf_token}}"
headers:
Content-Type: application/x-www-form-urlencoded
matchers:
- type: word
words:
- "Dashboard"
- "Welcome"

Extractors

extractors:
# Regex extractor
- type: regex
name: extracted_value
part: body
regex:
- '"token":"([^"]+)"'
group: 1

# JSON extractor (JQ-style)
- type: json
name: user_id
json:
- '.data.user.id'

# XPath extractor
- type: xpath
xpath:
- '//input[@name="token"]/@value'

# KV extractor
- type: kval
kval:
- Set-Cookie # extracts Set-Cookie header value

Variables and Helper Functions

Built-in variables

{{BaseURL}}            # https://target.com
{{RootURL}} # https://target.com (no path)
{{Host}} # target.com
{{Path}} # /current/path
{{Scheme}} # https
{{Port}} # 443
{{Username}} # from -var username=x
{{Password}} # from -var password=x
{{randstr}} # random string
{{rand_int(0,100)}} # random integer
{{unix_time()}} # current unix timestamp

DSL helpers

# String functions
contains(body, "error")
startswith(body, "HTTP")
endswith(header, "PHP")
len(body)
to_lower(body)
to_upper(header)
trim(body)
replace(body, "old", "new")
regex("pattern", body)

# Encoding
base64(body)
base64_decode(body)
url_encode("string")
url_decode("string")
md5("string")
sha256("string")
hex_encode("string")

# Date
date("2006-01-02")
unix_time()

OOB (Out-of-Band) Interaction

Use {{interactsh-url}} for blind vulnerability detection (DNS/HTTP callbacks):

id: blind-ssrf-example
info:
name: Blind SSRF via URL param
severity: high

requests:
- method: GET
path:
- "{{BaseURL}}/fetch?url=http://{{interactsh-url}}/test"

matchers:
- type: word
part: interactsh_protocol
words:
- "http"
- "dns"
condition: or

Nuclei automatically handles the Interactsh server — results appear in output when the callback fires.


Path Traversal Template

id: lfi-path-traversal
info:
name: Local File Inclusion - /etc/passwd
author: yourhandle
severity: high
tags: lfi,traversal

requests:
- method: GET
path:
- "{{BaseURL}}/{{path}}?page=../../../../etc/passwd"
- "{{BaseURL}}/{{path}}?file=../../../../etc/passwd"
- "{{BaseURL}}/{{path}}?include=../../../../etc/passwd"
- "{{BaseURL}}/{{path}}?lang=../../../../etc/passwd"

payloads:
path:
- ""
- "index.php"
- "page.php"

attack: clusterbomb # clusterbomb | pitchfork | sniper

matchers:
- type: regex
regex:
- "root:.*:0:0:"
part: body

Credential Stuffing Template

id: default-creds-check
info:
name: Default Credentials Check
severity: medium

requests:
- method: POST
path:
- "{{BaseURL}}/login"
body: "username={{username}}&password={{password}}"
headers:
Content-Type: application/x-www-form-urlencoded

payloads:
username:
- admin
- administrator
- root
password:
- admin
- password
- 1234
- ""

attack: pitchfork

matchers:
- type: dsl
dsl:
- 'status_code == 302 || contains(body, "dashboard")'

DNS Template

id: dns-mx-record
info:
name: MX Record Lookup
severity: info

dns:
- name: "{{FQDN}}"
type: MX

matchers:
- type: word
words:
- "google.com"
- "mailchimp"
part: answer

Running Templates

# Single template
nuclei -u https://target.com -t templates/mytemplate.yaml

# Directory of templates
nuclei -u https://target.com -t custom-templates/

# From URL list
nuclei -l urls.txt -t templates/

# Filter by tag
nuclei -u https://target.com -tags lfi,ssrf,rce

# Filter by severity
nuclei -u https://target.com -severity critical,high

# Validate template syntax
nuclei -validate -t mytemplate.yaml

# Update built-in templates
nuclei -update-templates

# Debug output
nuclei -u https://target.com -t mytemplate.yaml -debug

# Rate limiting
nuclei -u https://target.com -t templates/ -rate-limit 50

Template Writing Tips

  1. Test your matchers carefully — false positives waste client time. Use -debug to see raw responses.
  2. Use DSL conditions for complex logic instead of stacking multiple simple matchers.
  3. Always include a negative test — run against a URL that shouldn’t match and verify it doesn’t fire.
  4. OOB over in-band for blind vulnerabilities — more reliable than timing-based detection.
  5. Tag templates properly — good tags make templates reusable across different scopes.
  6. Version your templates with metadata.max-request to document expected request count.

Discussion

Leave a comment · All fields required · No spam

No comments yet. Be the first.