nuclei/pkg/tmplexec/flow
Tarun Koyalwar 619396a6b8
flow: flatten dynamic values array if len is 1 (#4625)
* flow: flatten dynamic values array if len is 1

* wait for exporters when closing

* misc updates
2024-01-13 00:44:25 +05:30
..
builtin nuclei v3 : misc updates (#4247) 2023-10-17 17:44:13 +05:30
testcases nuclei v3 : misc updates (#4247) 2023-10-17 17:44:13 +05:30
README.md nuclei v3 : misc updates (#4247) 2023-10-17 17:44:13 +05:30
doc.go nuclei v3 : misc updates (#4247) 2023-10-17 17:44:13 +05:30
flow_executor.go fix missing results in flow template + feature: internal matchers using `internal: true` (#4582) 2024-01-08 05:12:11 +05:30
flow_executor_test.go introduce scan context (#4373) 2023-11-28 00:24:45 +05:30
flow_internal.go flow: flatten dynamic values array if len is 1 (#4625) 2024-01-13 00:44:25 +05:30
util.go nuclei v3 : misc updates (#4247) 2023-10-17 17:44:13 +05:30

README.md

flow

flow is a new template engine/backend introduced in v3 which primarily adds 2 most awaited features

  • conditional execution of requests (ex: flow: dns() && http())
  • request execution orchestration (iterate over a slice, request execution order, if/for statement)

both of these features are implemented using javascript (ECMAScript 5.1) with goja backend.

conditional execution

Many times when writing complex templates we might need to add some extra checks (or conditional statements) before executing certain part of request

An ideal example of this would be when bruteforcing wordpress login with default usernames and passwords. If we try to write a template for this it would be something like this

id: wordpress-bruteforce
info:
  name: WordPress Login Bruteforce
  author: pdteam
  severity: high

http:
  - method: POST
    path:
      - "{{BaseURL}}/wp-login.php"
    payloads:
      username:
        - admin
        - guest
        - testuser
      password:
        - password123
        - qwertyuiop
        - letmein
    body: "log=§username§&pwd=§password§&wp-submit=Log+In"
    attack: clusterbomb 
    matchers:
      - type: word
        words:
          - "ERROR"
        part: body
        negative: true

but if we carefully re-evaluate this template, we can see that template is sending 9 requests without even checking, if the url actually exists or target site is actually a wordpress site. before v3 it was possible to do this by adding a extractor and sending additional content in say url fragment and it would fail if request was not successful and another way would be writing a workflow (2 templates and 1 workflow file i.e total 3 files for 1 template) but this is hacky and not a good solution.

With addition of flow in Nuclei v3 we can re-write this template to first check if target is a wordpress site, if yes then bruteforce login with default credentials and this can be achieved by simply adding one line of content i.e flow: http("check-wp") && http("bruteforce") and nuclei will take care of everything else.

id: wordpress-bruteforce
info:
  name: WordPress Login Bruteforce
  author: pdteam
  severity: high

flow: http("check-wp") && http("bruteforce")

http:
  - id: check-wp
    method: GET
    path:
      - "{{BaseURL}}/wp-login.php"
    
    matchers:
        - type: word
            words:
            - "WordPress"
            part: body
        - type: word
            words:
            - "wp-content"
            part: body
    matchers-condition: and

  - id: bruteforce
    method: POST
    path:
      - "{{BaseURL}}/wp-login.php"
    payloads:
      username:
        - admin
        - guest
        - testuser
      password:
        - password123
        - qwertyuiop
        - letmein
    body: "log=§username§&pwd=§password§&wp-submit=Log+In"
    attack: clusterbomb 
    matchers:
      - type: word
        words:
          - "ERROR"
        part: body
        negative: true

Note: this is just a example template with poor matchers. refer 'nuclei-templates' repo for final template

The update template now seems straight forward and easy to understand. we are first checking if target is a wordpress site and then executing bruteforce requests. This is just a simple example of conditional execution and flow accepts any Javascript (ECMAScript 5.1) expression/code so you are free to craft any conditional execution logic you want using for,if and whatnot.

request execution orchestration

conditional execution is one simple use case of flow but flow is much more powerful than that, for example it can be used to

  • iterate over a slice of values and execute requests for each value (ex: dns-flow-probe)
  • extract values from one request and iterate over them and execute requests for each value ex: dns-flow-probe
  • get/set values from/to template context (global variables)
  • print/log values to stdout at xyz condition or while debugging
  • adding custom logic during template execution (ex: if status code is 403 => login and then re-run it)
  • use any/all ECMAScript 5.1 javascript (like objects,arrays etc) and build/transform variables/input at runtime
  • update variables at runtime (ex: when jwt expires update it by using refresh token and then continue execution)
  • and a lot more (this is just a tip of iceberg)

simply put request execution orchestration can be understood as nuclei logic bindings for javascript (i.e two way interaction between javascript and nuclei for a specific template)

To better understand orchestration we can try to build a template for vhost enumeration using flow. which usually requires writing / using a new tool

for basic vhost enumeration a template should

  • do a PTR lookup for given ip
  • get SSL ceritificate for given ip (i.e tls-grab)
    • extract subject_cn from certificate
    • extract subject_alt_names(SAN) from certificate
    • filter out wildcard prefix from above values
  • and finally bruteforce all found vhosts

Now if we try to implement this in template it would be

# send a ssl request to get certificate
ssl:
  - address: "{{Host}}:{{Port}}"

# do a PTR lookup for given ip and get PTR value
dns:
  - name: "{{FQDN}}"
    type: PTR

    matchers:
      - type: word
        words:
          - "IN\tPTR"

    extractors:
      - type: regex
        name: ptrValue
        internal: true
        group: 1
        regex:
          - "IN\tPTR\t(.+)" 

# bruteforce all found vhosts
http:
  - raw:
      - |
        GET / HTTP/1.1
        Host: {{vhost}}        

    matchers:
      - type: status
        negative: true
        status:
          - 400
          - 502

    extractors:
      - type: dsl
        dsl:
          - '"VHOST: " + vhost + ", SC: " + status_code + ", CL: " + content_length'                                                                                tarun@macbook:~/Codebase/nuclei/integration_tes

But this template is not yet ready as it is missing core logic i.e how we use all these obtained data and do bruteforce and this is where flow comes into picture. flow is javascript code with two way bindings to nuclei. if we write javascript code to orchestrate vhost enumeration it is as simple as

  ssl();
  dns();
  for (let vhost of iterate(template["ssl_subject_cn"],template["ssl_subject_an"])) {
    set("vhost", vhost);
    http(); }

With just extra 5 lines of javascript code template can now perform vhost enumeration and this can be run on scale with all awesome features of nuclei with various supported inputs like ASN,CIDR,URL etc

In above Js code we are using some Nuclei JS bindings lets understand what they do

  • ssl() => execute ssl request
  • dns() => execute dns request
  • template["ssl_subject_cn"] => get value of ssl_subject_cn from template context (global variables)
  • iterate() => this is a nuclei helper function which iterates any type of value (array,map,string,number) while handling empty / nil values
  • set("vhost",vhost) => creates new variable vhost in template and assigns value of vhost to it
  • http() => execute http request

This template is now missing one last thing i.e

  • removing wildcard prefix (*.) in subject_cn,subject_an
  • trailing . in PTR value

and this can be done using either JS methods of using DSL helper functions as shown in below template

id: vhost-enum-flow

info:
  name: vhost enum flow
  author: tarunKoyalwar
  severity: info
  description: |
    vhost enumeration by extracting potential vhost names from ssl certificate and dns ptr records    

flow: |
  ssl();
  dns({hide: true});
  for (let vhost of iterate(template["ssl_subject_cn"],template["ssl_subject_an"])) {
    vhost = vhost.replace("*.", "")
    set("vhost", vhost);
    http();
  }  

ssl:
  - address: "{{Host}}:{{Port}}"

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

    matchers:
      - type: word
        words:
          - "IN\tPTR"

    extractors:
      - type: regex
        name: ptrValue
        internal: true
        group: 1
        regex:
          - "IN\tPTR\t(.+)" 

http:
  - raw:
      - |
        GET / HTTP/1.1
        Host: {{trim_suffix(vhost, ".")}}        

    matchers:
      - type: status
        negative: true
        status:
          - 400
          - 502

    extractors:
      - type: dsl
        dsl:
          - '"VHOST: " + vhost + ", SC: " + status_code + ", CL: " + content_length'

Nuclei JS Bindings

This section contains a brief description of all nuclei JS bindings and their usage

1. Protocol Execution Functions

Any protocol that is present in a nuclei template can be called/executed in javascript in format proto_name() i.e http() , dns() , ssl() etc If we want to execute a specific request of a protocol (ref: see nuclei-flow-dns) this can be achieved by either passing

  • index of that request in protocol (ex: dns(0), dns(1) etc)
  • id of that request in protocol (ex: dns("extract-vps"), dns("probe-http") etc) For More complex use cases multiple requests of a single protocol can be executed by just specifying their index or id one after another (ex: dns("extract-vps","1"))

2. Iterate Helper Function

Iterate is a nuclei js helper function which can be used to iterate over any type of value (array,map,string,number) while handling empty / nil values. This is addon helper function from nuclei to omit boilerplate code of checking if value is empty or not and then iterating over it

iterate(123,{"a":1,"b":2,"c":3})
// iterate over array with custom separator
iterate([1,2,3,4,5], " ")

Note: In above example we used iterate(template["ssl_subject_cn"],template["ssl_subject_an"]) which removed lot of boilerplate code of checking if value is empty or not and then iterating over it

3. Set Helper Function

When Iterating over a values/array or some other use case we might want to invoke a request with custom/given value and this can be achieved by using set() helper function. When invoked/called it adds given variable to template context (global variables) and that value is used during execution of request/protocol. the format of set() is set("variable_name",value) ex: set("username","admin") etc

  for (let vhost of myArray) {
  set("vhost", vhost);
  http(1)
}

Note: In above example we used set("vhost", vhost) which added vhost to template context (global variables) and then called http(1) which used this value in request

4. Template Context

when using nuclei -jsonl flag we get lot of data/metadata related to a vulnerability (ex: template details,extracted-values and much more) . A template context is nothing but a map/JSON containing all this data along with internal/unexported data that is only available at runtime (ex: extracted values from previous requests, variables added using set() etc). This template context is available in javascript as template variable and can be used to access any data from it. ex: template["ssl_subject_cn"] , template["ssl_subject_an"] etc

template["ssl_subject_cn"] // returns value of ssl_subject_cn from template context which is available after executing ssl request 
template["ptrValue"]  // returns value of ptrValue which was extracted using regex with internal: true

Lot of times we don't known what all data is available in template context and this can be easily found by printing it to stdout using log() function

log(template)

5. Log Helper Function

It is a nuclei js alternative to console.log and this pretty prints map data in readable format Note: This should be used for debugging purposed only as this prints data to stdout

6. Dedupe

Lot of times just having arrays/slices is not enough and we might need to remove duplicate variables . for example in earlier vhost enumeration we did not remove any duplicates as there is always a chance of duplicate values in ssl_subject_cn and ssl_subject_an and this can be achieved by using dedupe() object. This is nuclei js helper function to abstract away boilerplate code of removing duplicates from array/slice

let uniq = new Dedupe(); // create new dedupe object
uniq.Add(template["ptrValue"]) 
uniq.Add(template["ssl_subject_cn"]);
uniq.Add(template["ssl_subject_an"]); 
log(uniq.Values())

And that's it , this automatically converts any slice/array to map and removes duplicates from it and returns a slice/array of unique values


Similar to DSL helper functions . we can either use built in functions available with Javscript (ECMAScript 5.1) or use DSL helper functions and its upto user to decide which one to uses