Crafting Custom Nuclei Templates for Rapid Vulnerability Detection

Crafting Custom Nuclei Templates for Rapid Vulnerability Detection

Rapid vulnerability detection often hinges on specialized tooling and custom logic tailored to specific application contexts. Nuclei, a fast and customizable scanner, provides this capability through its template engine. Instead of relying solely on public signatures, crafting custom Nuclei templates allows pentesters to codify unique detection heuristics, specific exploit patterns for zero-days or N-days, or to test for configurations unique to an engagement. This direct approach enables quick deployment of targeted checks, significantly accelerating the identification phase within a pentest or bug bounty workflow.

The Anatomy of a Nuclei Template

Understanding the fundamental structure of a Nuclei template is critical before building anything complex. Every template is a YAML file, defining a request (or a series of requests) and the conditions under which a match indicates a vulnerability or interesting finding.

Template Metadata: id and info

The id field is a unique identifier for your template. It's crucial for managing and referencing templates. The info block provides metadata such as the template's name, author, severity, and tags, which help categorize and filter templates during scans.

id: custom-admin-panel-detect

info:
  name: Custom Admin Panel Detection
  author: your-pentester-name
  severity: info
  description: "Detects a specific custom admin panel based on unique page title."
  reference:
    - https://example.com/docs/admin_panel_signature
  classification:
    cvss-metrics: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N
    cvss-score: 0.0
    cwe-id: CWE-200
  tags: custom, admin, panel, discovery

Defining Requests: http and network

The core of most Nuclei templates lies within the http or network sections, defining the actual interactions with the target. For web applications, http is predominant. This section specifies the HTTP method, path, headers, and body content. A simple GET request to check for a specific path:

requests:
  - method: GET
    path:
      - "{{BaseURL}}/custom/admin/login.php"
    headers:
      User-Agent: "Nuclei-Custom-Scanner/1.0"
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "Custom Admin Login"
        part: body
For POST requests, include the body directive. Variables like {{BaseURL}} are automatically resolved by Nuclei from your target list.

requests:
  - method: POST
    path:
      - "{{BaseURL}}/api/v1/auth"
    headers:
      Content-Type: "application/json"
    body: |
      {
        "username": "admin",
        "password": "password123"
      }
    matchers:
      - type: status
        status:
          - 401
      - type: word
        words:
          - "Invalid Credentials"
        part: body

Precision Matching: Identifying Vulnerabilities

After sending a request, Nuclei evaluates the response against defined matchers. These matchers are the intelligence layer, determining if the response indicates the presence of the vulnerability or condition you're looking for.

Response Matchers: status, word, regex

  • status: Matches against the HTTP response status code. Useful for checking for successful page loads (200), redirects (3xx), or errors (4xx, 5xx).
  • word: Searches for specific strings or keywords within the response body, headers, or other parts. Supports case-insensitive matching and exclusion.
  • regex: Provides powerful pattern matching using regular expressions. Essential for dynamic content or specific data formats.
Combining these matchers allows for precise detection. The type: or or type: and directives within the matchers block control the logic.

matchers:
  - type: status
    status:
      - 200
  - type: word
    words:
      - "Welcome to the Dashboard"
      - "Admin Area"
    condition: or
    part: body
  - type: regex
    regex:
      - "version: v[0-9]+\\.[0-9]+\\.[0-9]+"
    part: header
    case-insensitive: true
In the example above, a match occurs if the status is 200 AND (either "Welcome to the Dashboard" or "Admin Area" is found in the body) AND a version string is found in the header.

Advanced Matching with DSL

Nuclei's Domain Specific Language (DSL) matchers allow for complex, programmatic checks against various parts of the HTTP response. This includes evaluating response sizes, specific header values, or even performing logical operations on extracted data. Example using DSL to check for a specific header value and body length:

matchers:
  - type: dsl
    dsl:
      - 'contains(all_headers, "X-Powered-By: Custom-Framework")'
      - 'len(body) > 1000'
    condition: and
This matcher will trigger only if the X-Powered-By header contains "Custom-Framework" and the response body is longer than 1000 characters.

Data Extraction for Further Analysis

Sometimes, merely detecting a condition isn't enough; you need to extract specific data from the response for further analysis or to chain into subsequent requests. Nuclei provides extractors for this purpose.

Extracting Information: regex, json, xpath

  • regex: Extracts data based on regular expressions, often used with capturing groups.
  • json: Extracts values from JSON responses using JSONPath expressions.
  • xpath: Extracts values from XML/HTML responses using XPath expressions.
Extracting a CSRF token using regex from the body:

extractors:
  - type: regex
    name: csrf_token
    regex:
      - 'name="_csrf" value="([a-zA-Z0-9]{32})"'
    group: 1
    part: body
Extracting a session ID using JSONPath:

extractors:
  - type: json
    name: session_id
    json:
      - '$.data.session'
    part: body

Chaining Requests and Variables

One of Nuclei's powerful features is the ability to chain requests, where data extracted from one request can be used as input for a subsequent request. This is critical for multi-step vulnerabilities like authentication bypasses or information disclosure through chained actions. The set directive within an extractor populates a variable that can then be referenced in later requests using {{variable_name}}.

id: chained-auth-bypass

info:
  name: Chained Authentication Bypass
  author: your-pentester-name
  severity: high
  description: "Demonstrates a two-step authentication bypass."
  tags: auth, bypass, chain

requests:
  - raw:
      - |
        GET /login HTTP/1.1
        Host: {{Hostname}}
        User-Agent: Nuclei-Custom-Scanner/1.0

    matchers:
      - type: status
        status:
          - 200
    extractors:
      - type: regex
        name: auth_token
        regex:
          - 'name="auth_token" value="([a-zA-Z0-9]+)"'
        group: 1
        part: body
        set: auth_token

  - raw:
      - |
        POST /verify HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded
        User-Agent: Nuclei-Custom-Scanner/1.0
        Content-Length: {{len("auth_token=" + auth_token)}}

        auth_token={{auth_token}}

    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "Authentication Successful"
        part: body
In this template, the first request fetches an auth_token, which is then dynamically used in the body of the second POST request.

Interacting with External Services: Interactsh Integration

For out-of-band (OOB) vulnerability detection, such as blind SSRF, XSS, or RCE, Nuclei integrates seamlessly with Interactsh. By embedding an Interactsh URL in your request, you can detect if the target application makes an external call, confirming the vulnerability. To use Interactsh, simply include {{interactsh-url}} in your request where you expect an OOB interaction. Nuclei automatically manages Interactsh server interaction.

id: blind-ssrf-detection

info:
  name: Blind SSRF via Image URL
  author: your-pentester-name
  severity: high
  description: "Detects blind SSRF by attempting to load an image from Interactsh."
  tags: ssrf, oob, interactsh

requests:
  - method: GET
    path:
      - "{{BaseURL}}/load_image?url={{interactsh-url}}"
    matchers:
      - type: word
        words:
          - "image loaded successfully" # Optional, if the app responds
        part: body
      - type: dsl
        dsl:
          - 'interactsh_protocol("http")' # Verifies an HTTP interaction occurred
        condition: and
The interactsh_protocol DSL function is crucial here, confirming that an HTTP request was made to the Interactsh server by the target.

Testing and Debugging Your Custom Templates

Effective template development requires rigorous testing and debugging. Nuclei provides several command-line flags to assist in this process. To test a template against a specific target and get verbose output, use the -debug and -validate flags:

nuclei -t custom-admin-panel-detect.yaml -u https://example.com -debug -validate
  • -debug: Shows the full HTTP requests and responses, including any internal variables or extracted data. This is invaluable for understanding why a matcher isn't triggering or an extractor isn't capturing data as expected.
  • -validate: Checks the template YAML for syntax errors and structural issues before execution.
  • -silent: Can be useful in conjunction with -debug to suppress normal Nuclei output and focus solely on debug messages.
  • -lfa: (List Field Attributes) Displays all available fields that can be accessed via DSL for a given template and target.
When debugging, pay close attention to:
  • **Request formation**: Does the generated HTTP request match what you intended? Check headers, paths, and body.
  • **Response content**: Does the target's response contain the expected keywords, regex patterns, or JSON structure?
  • **Matcher logic**: Is your `condition: and` or `condition: or` correctly implemented? Are all parts (body, header, all_headers) specified correctly?
  • **Extractor output**: Are the extracted variables being populated correctly? Use `debug` to see their values.
A common pitfall is incorrect regular expression syntax or JSONPath expressions. Always test these independently before embedding them in a Nuclei template. If a template isn't firing, start by simplifying your matchers and extractors, gradually adding complexity back in.