CAPABILITY

A finding is something the target had to do.

Most scanners tell you what might be true. This one makes the application prove it. If nothing had to be computed, fetched or decided, it is not a finding. It is a guess with a severity label stapled to it.

  • 15/15 Detection benchmark
  • 99/99 Template pack
  • 9/9 Injection oracle
  • 48/48 The full lab
  • 22 Classes covered
we send 199933 * 314573 an expression, never its answer
a page that echoes returns 199933 * 314573 not a finding
a shell that ran it returns 62893523609 a finding
Blind command injection, in three lines.

Coverage

22 classes, and what proves each one.

Click any class to see what the application has to actually do before we will write it down.

Injection8

Every one of these is confirmed by something the application had to compute or fetch, never by the payload coming back.

Out-of-band3

Classes where the response tells you nothing, so the target has to contact something instead.

Serialization and parsing2

The class most scanners can only find in Java, because Java is the one language where somebody already wrote the universal gadget chain for them.

Business logic4

The classes no payload list will ever reach, because nothing is malformed. The request is perfectly valid. The application is just wrong.

Access and exposure5

What a caller can reach that belongs to somebody else, or should not be reachable at all.

Standards

What it covers, and what it will not claim.

Pick a list. Every row that says yes names the class that does the work, and every row that says no says why, because a coverage matrix with no gaps in it is a brochure.

8 covered 1 partly 1 not claimed The list this engine was built against. Eight fully, one partly, and one that nothing on the outside can honestly report.

MITRE ATT&CK is deliberately not here. It describes what an adversary does across a whole intrusion, and almost all of a web scanner collapses into one technique in it. A six-row ATT&CK matrix would look impressive and tell you nothing, which is the opposite of what this section is for.

The unusual part

Four things most scanners will not do.

  • Deserialization outside Java

    Make the parser name itself on bytes no format can read, then go quiet on valid input. Both halves, or nothing.

  • Chains, with the coincidence removed

    An SSRF that fetched the Redis another engine found. One probe showed both halves, or it is not reported.

  • What a file read is worth

    /etc/passwd proves traversal and nothing else. A confirmed read goes looking for the config beside it. Key names recorded, never values.

  • Bugs with nothing malformed in them

    A checkout that skips the charge. A coupon redeemed eight times at once. No payload list finds these.

Behind the login

A scanner that stops at the login page is testing a different application.

An unauthenticated scan is not a lighter version of a real test. It is a thorough test of a much smaller application that happens to share a domain with yours.

Login typeNotes
Bearer tokensStaticA header you already hold.
Custom headersStaticWhatever your API actually reads.
CookiesStaticA session captured from a browser.
HTTP basicStaticStill everywhere, still worth testing behind.
Form loginInteractiveSubmit a username and password, keep the session that comes back.
OAuth without a browserInteractiveClient-credentials and password grants, run headless.
OAuth, SSO and MFAInteractiveAuthorization-code flows that need a real browser driven through them. This is the one most tools give up on.

Where the password goes

  1. Youhand over a login, once
  2. Vaultencrypted at rest, resolved by reference
  3. Brokersigns in, returns a short-lived token
  4. Nodeonly ever sees the token

That broker used to open your login page from our address, breaking a rule we had written down ourselves. Found in an audit, fixed, and written up in whose IP address is your scanner using?

Broken authorization

The class where nothing in the request is wrong.

One user reading another user's data. First on the OWASP API list, and the class automated tools are worst at, because a perfectly valid request from the wrong person is the vulnerability.

Three names for one mistake

NameLevelWhat happens
BOLAobjectYou ask for order 1043. It is somebody else's. You get it.
BFLAfunctionA normal account calls an admin action and the server does it.
BOPLApropertyYou send role or verified and it sticks.

Replay as everyone, then compare

You supply the logins. Each endpoint is sent as every identity.

Signed in asWhat it catches
adminThe privileged baseline. What a legitimate high-privilege caller can reach.
user AAn ordinary account that owns some objects.
user BA second ordinary account, used to reach for user A's objects. This is the one that finds BOLA.
anonymousNo credentials at all, for the endpoints that forgot to ask.
admin 200 user A's order allowed, and correct
user A 200 user A's order the owner
user B 200 user A's order nobody checked who asked
anonymous 401 nothing correctly refused
GET /api/orders/1043, an order belonging to user A. Four identical requests. Only the third one is the bug, and nothing about it is malformed.

An admin reading a user's object is the job, not a bug, so direction across the boundary counts. Every finding is re-issued and has to reproduce before it reaches you.

We point this at ourselves too, which is how we found an assistant that could read other people's data.

Correctness

Every false positive we shipped had one cause.

The scanner accepted something it had caused as evidence. Four classes, four bits of code, one mistake made four times. It is a stupid mistake and it is very easy to make.

  • Remote code executiona page echoing our own user agent
  • Seven file readsa docs page full of PHP examples
  • SSRF, confirmedour own redirect-following client
  • Nine blind SQL injectionsour worker changing the admin password mid-scan

9 rules came out of it, enforced in code rather than in review:

They cost us real findings. That trade, and the one rule that turned out to be wrong, is in every false positive we have shipped had the same cause.

Evidence

Numbers, and what produced each one.

These come from our own harnesses, so treat them as our homework rather than an audit. One of them is published in full, with every miss sorted by cause and every false positive named and counted against us, and that is the only one you can mark yourself.

  • 15/15

    detected, no false positives

    Detection benchmark

    Includes targets built to fool a scanner, one of which serves the words "Index of /" as ordinary prose.

  • 99/99

    template fixtures fired, none cross-fired

    Template pack

    Every template in the pack now has a target that must make it fire, plus one it must stay silent on. The single exception is Log4Shell, which only ever fires on an out-of-band callback, so it is proved by the OAST harness instead and says so by name.

  • 9/9

    found, nothing missed, nothing invented

    Injection oracle

    Nineteen endpoints, ten of them safe by construction.

  • 48/48

    answer-key entries, keys written first

    The full lab

    Nineteen deliberately vulnerable targets with answer keys written before the scan, plus one whose correct score is silence. This is one lab and it is finite; the number to watch is what happens when entries are added to it, not this one.

    Read the report
  • 124

    adversarial cases, no crashes

    Robustness

    Five never finish, against servers that never end a response. They hit the engine deadline. Stated because a scorecard without its remainder is not one.

The lab run is the one worth reading, because it is the only one here that is capable of reporting a bad result, and it has.

Templates

100 of them, and you can read every one.

A detection rule you cannot read is one you are trusting on vibes. These are Apache-2.0 and editable, in the Nuclei format, so packs you already have will mostly just work.

  • technologies 30
  • exposures 20
  • CVEs 15
  • vulnerabilities 15
  • panels 10
  • misconfigurations 8
  • default logins 2

One of them, whole

Twenty-two lines, copied out of the pack rather than written for this page. Note the and: a 200 on its own proves nothing, because plenty of servers answer 200 for a path that does not exist, so the body has to look like a real git HEAD file too. That is this whole page in one rule.

id: git-head-exposure
info:
  name: Exposed .git/HEAD
  severity: medium
  description: A readable .git/HEAD confirms the .git directory is
    web-served, which typically allows reconstructing the full source
    tree (and any secrets committed to it).
  tags: exposure,git,source-code
  metadata:
    category: exposures
http:
  - method: GET
    path:
      - "{{BaseURL}}/.git/HEAD"
    matchers-condition: and
    matchers:
      - type: regex
        part: body
        regex:
          - "^ref:\s+refs/heads/"
      - type: status
        status:
          - 200
exposures/git-head.yaml

Nuclei and cortex, on the same file

These are not rival formats and it would be dishonest to draw them that way. The template above runs in both. What differs is what each engine does with it.

Read by both

  • id, info, severity, description and tags
  • http requests, or the older requests spelling
  • method, path, headers, body and payload lists
  • matchers: word, regex, status, size and dsl
  • matchers-condition, per-matcher condition and negative
  • unsafe, for probes that need the raw request target

Nuclei

A bigger tool at the format level, and we are not going to pretend otherwise.

  • Protocols beyond HTTP: dns, network, ssl, file, code, javascript and headless
  • Workflows, for chaining templates conditionally
  • Extractors, for pulling values out of one response and into the next
  • Variables and helper functions across a much larger DSL
  • A public template repository with thousands of community rules

Cortex

Reads the subset above and adds two rules the format itself has no opinion about. Both are tests, so a template that breaks one fails the build rather than reaching a customer.

  • A template may not match on a word it transmits, unless it is tagged reflection and says so in writing
  • Every template must declare its class in its tags, so no finding falls through to a generic bucket
template `{id}` matches on `{word}`, which it sends itself - an echo
would satisfy it. Use a marker the target can only produce by executing
something, or tag the template `reflection`.

Templates are the commodity half, open on purpose. The oracles above are not templates at all, which is why a template count is a poor way to compare two scanners and everybody keeps doing it anyway.

Limits

Where it is weak.

5 of 124 robustness cases never finish
Servers that never end a response hit our own deadline. No crashes, no memory blowups.
Race conditions and request smuggling
Cannot be tested without changing the target, so they are opt-in and capped.
Flow-order abuse
Needs a recording of the real flow, not a crawl.
Forms that change a credential
Never submitted, so some vulnerable endpoints get reported as present and then left completely alone.
One template is not proved here
Log4Shell only ever fires on an out-of-band callback and declares no response matcher at all, so a fixture that answers the request would prove nothing. It is proved by the OAST harness instead, and named in the scorecard rather than quietly counted.

It sends real payloads, and only ever at hosts inside the scan target. Run it against things you own or have written permission to test.

Questions

Frequently asked.

Point it at something you are allowed to break.

Free tier, no card.