l1ackers
Not marked. Sign in and it follows you. Next: 056: nikto, and the honest banner

~/tutorials/055-what-a-scanner-cannot-know.md

055: What a scanner cannot know

The last lesson taught the strongest case, perfect recall of the target's own testimony. This one is the other half, and it is the half that decides whether you are a tester or a scanner operator. A scanner cannot know what the application does, because what an application does is not stated in any header, version, or file that answers. It is coded behavior, and no amount of asking eight thousand questions about the doorway will tell you what happens in the room.

What this lesson is for

This lesson builds a shop with one bug in it, runs the scanners over it, and shows you the size of the miss. Not to dismiss scanners, whose recall of testimony you already receipted, but to fix their weight in your workflow: they are the front of the process, never the whole of it. The bug class demonstrated here, the server trusting a number the client sent, is not exotic. It is in real applications this week, and scanners walk past it every day, by design.

What the reader meets

A shop of two items and eleven lines of substance. You build it yourself, and the build is the first half of the exercise: the whole shop, the bug included, is eleven lines of Python on the apparatus page the web lab, one docker command to run. Every address in this lesson, like the 172.31.14.3 below, is a private docker address from that page's network, minted when the lab was built and different on your machine, and the page's first section teaches the one command that prints your own. The scanners arrive the same way they did last lesson, from the page's scanner seat, the kaliscan container, and the curl lines that fire the bug run from there too. One endpoint lists an item and its price. One endpoint adds to the cart, and the cart totals the order and answers with what it charged. The shop's catalog says a shirt costs two thousand cents. The bug: the add endpoint computes the total from the price the request submitted, not from the catalog. The server trusts the client's number.

First, the honest purchase, price submitted faithfully:

$ curl -s -X POST http://172.31.14.3:5001/api/cart/add \
    -H "Content-Type: application/json" \
    -d '{"name":"shirt","price":2000,"qty":1}'
{"charged":2000,"name":"shirt","qty":1,"total":2000}

Two thousand cents charged for a two thousand cent shirt. Now the same request with one number changed, the price the client claims:

{"charged":1,"name":"shirt","qty":1,"total":1}

One cent. The server charged what the buyer said the item cost. There is no exploit payload in this, no quote mark, no script tag, nothing a signature library has ever matched on. It is a two thousand cent discount, taken by editing a number that was never meant to be editable, and if the shop had a payments step, this receipt would be money.

Now the scanners, over the same shop, same bug, same minutes. whatweb first:

http://172.31.14.3:5001/ [200 OK] Country[RESERVED][ZZ],
HTTPServer[Werkzeug/3.1.8 Python/3.12.14], IP[172.31.14.3],
Python[3.12.14], Werkzeug[3.1.8]

Accurate, and completely beside the point. Every fact true: it is a Python server, running Werkzeug, version 3.1.8. Not one byte of that addresses the fact that the shop gives merchandise away to anyone who edits a JSON field. And nikto, the full census:

+ Server: Werkzeug/3.1.8 Python/3.12.14
+ [013587] /: Suggested security header missing: x-content-type-options
+ [013587] /: Suggested security header missing: strict-transport-security
+ [013587] /: Suggested security header missing: content-security-policy
+ 8246 requests: 0 errors and 9 items reported on the remote host

Eight thousand two hundred and forty six requests. Nine findings, all of them header suggestions, none of them about the money. The scanner fired more requests at this eleven line shop than a human tester would fire at a real application, and the bug is not hiding. It fires on every single add to cart. It is the application's central behavior, and the census could not see it, because the census asks what the site says and what the site has, and this bug is in what the site does.

The instrument

Why the miss is structural, not fixable by better scanners. Divide what an application is into two halves. The stated half: versions, headers, files, forms, everything that exists as a fact a response can carry. The behavioral half: what the code does with input, which prices are trusted, which states can be skipped, which actions repeat, which user can touch which record. Scanners, as the last lesson established, are testimony collectors. The behavioral half states nothing. It only reveals itself when a specific input meets a specific code path, and the space of those meetings is not enumerable by checklist. A scanner would have to buy the shirt with every possible price to notice, and no scanner buys things. It has no concept of a purchase, an expectation, or a wrong answer, because those live in the business, not the bytes.

That is the boundary in one sentence, and it is the tier's thesis so far wearing its sharpest edge: the scanner reads the shipment. The tester reads what the shipment means. Lesson 049 taught you to read the body as a manifest. This lesson's bug was visible in the manifest all along, the price field in the add to cart request, and the scanner read that manifest eight thousand times without once asking the only question that mattered: should the server believe this number.

What finds this class, since scanners cannot, is a short method that scales. Map the application's verbs as business actions, buy, add, transfer, vote, refund, not as endpoints. For each action, write down what the client is trusted to decide, and what the server should be deciding alone. Price, quantity, discount, recipient, identity, currency, whether the previous step happened. Then, for each trusted-by-client item, change it and watch the answer. That method would have found this bug on the second item of the list, and it is the same discipline as lesson 049's constraint check, generalized from form attributes to business meaning. It needs the proxy from 050, to see and hold the request. It needs the conversation literacy of 047, to read what the client sent. It needs nothing automated at all.

Try it

  1. Build the shop from this lesson, eleven lines, the receipt above is the spec. Fire the honest purchase, then the forged one, and hold both requests in your proxy's history side by side. The one-number diff between them is the entire finding, and seeing it held in history is seeing the method work.
  2. Run whatweb and nikto against your shop and reconcile their reports with your own eyes: everything they say is true, and the charged one cent receipt is invisible. Write two sentences in your notes on what you would have reported if you had only the scanner outputs. That gap, between the scanner report and the truth, is your professional value stated in two sentences.
  3. Write the trusted-by-client list for a real application you use. Grocery delivery, chat, banking, any of them: what does the client send that the server should be deciding alone. Five items is a good first list, and every item on it is a question the application has to answer correctly somewhere, or a bug of this class is waiting.
Reveal the answer

The cannot-know ledger, the honest boundary of automated testing, each row receipted by this lesson's lab or the tier before it:

WHAT SCANNERS CANNOT KNOW

Business logic        what a price should be, whether this user may refund
                      that one, if the vote already counted. Lab: charged 1.

The code's intent     whether a value is a feature or a bug. The shop's
                      'price' field is invisible until you hold an expectation.

What never crosses
the wire              the catalog the server never sent. The scanner reads
                      requests and responses; the 2000-cent truth lived in
                      neither, and only the gap between them betrayed it.

Whether the finding
matters               8246 requests found 9 header notes on the shop, and the
                      one finding that mattered was absent. Severity labels
                      attach to rows; consequence lives in the business.

What to do next       scanners end at lists. The verb-mapping method of this
                      lesson is the beginning of judgment, and it is yours.

Read the ledger as a division of labor rather than a list of failures. The scanner's half of the work, testimony collection, is worth doing and worth automating, and the last lesson receipted why. This half, the meaning half, is what the client is actually paying for when they hire a person, and no tool's changelog has ever moved a row from this ledger to the last lesson's. Tools will keep trying. The trying is good, and the boundary keeps moving at its edges. But the center of this ledger, what the application should do versus what it does, is not a sensing problem, and a decade of machine effort has not made it one. Hold the scanner's output in one hand, this ledger in the other, and price your work accordingly.

What this lesson does not claim

The boundary is real but it is not a wall against all tooling, and honesty cuts both ways. Fuzzers, a later lesson, automate the asking of questions and will find classes this shop's checklist missed. Code analysis tools read the source you may not have. And some business logic bugs are so common they have become named, checkable patterns in the strongest commercial platforms. What this lesson claims is narrower and permanent: the map of what the application should do lives in the business, the code carries what it does, and the space between is traversed by a mind holding an expectation. The scanner has no expectations. That is not a limitation of current scanners. It is what the word scanner means.

Not marked. Sign in and it follows you. Next: 056: nikto, and the honest banner
l1ackers · shell this is a way in, not a requirement
$