LSN โ€” Local Sovereign Network

The Hits We Advertised: Five Weeks of Saying Yes to Files We Never Had

Letter I ended on a ratio: 32,460 requests hunting a credential in a file, against four offering one at the door. The conclusion was that nobody attacks the lock any more โ€” they check whether the key was left lying around.

This letter is about what happened when this estate answered them.

For roughly five weeks โ€” 2026-06-06 to 2026-07-08 โ€” two services on this domain replied HTTP 200, with a real page body, to any filename a scanner could invent. Not a 404. Not a redirect. A success code and a full page of content.

1,834 requests. 1,015 distinct invented paths. 98 distinct addresses.

Among the paths this estate confirmed as existing:

Path we answered 200 OKRequestsAddresses
/config.json87
/gcp-credentials.json77
/google-credentials.json76
/firebase-adminsdk.json77
/firebase-credentials.json66
/gcp.json66
/config/service-account.json66
/credentials.json65
/appsettings.Production.json66
/service-account.json55
/keyfile.json54
/key.json44

A Google Cloud service-account key is close to the most valuable single file a scanner can find on the open internet. It is a long-lived credential, frequently over-scoped, and it is JSON โ€” trivially machine-checkable. This estate spent five weeks telling every address that asked that it had one. Counting every spelling of a credential or service-account file that received a 200 across the two hosts: 54 distinct paths.

Nothing leaked. No credential was exposed, because none of those files exist. That is the point, and it is also the problem.

The corpus

SourceTraefik edge access log
Window2026-06-03 โ†’ 2026-08-11 (70 days)
Requests286,640
Distinct addresses26,846

Every figure below comes from that one source. Where the application's own behaviour is described, it is described from what the log recorded, and the two live checks in this letter were run against this estate's own hosts.

Who was asking

The population is shell_upload โ€” the second-largest intent in the corpus and, until now, the one The Census had not examined.

Distinct addresses693
Requests55,445

Its defining feature is what is missing from it:

User-AgentRequestsAddresses
(empty)53,628517
Mozilla/5.0 โ€ฆ Chrome/120.0.0.05976
libredtail-http464129
GenomeCrawlerd/1.0 (Nokia)37417
Go-http-client/2.02338
ipcrawl-exposure-scanner/1.0 (+authorized assessment)171

96.7% of this traffic carries no User-Agent at all. Corpus-wide, 936 addresses sent 77,278 requests with an empty UA. A browser does not do this. A well-built crawler does not do this โ€” even the impostors in Letter E bothered to claim a name. An empty User-Agent is what you get from a raw socket loop written by someone who never considered that the header existed, or who correctly judged that no one is checking.

What they were looking for

Not a vulnerability. Somebody else's back door.

PathHitsAddresses
/wp-content/plugins/hellopress/wp_filemanager.php804491
/this_is_a_new_hello_world.php778491
/x.php429299
/222.php369224
/edit.php366220
/admin.php349219
/wp-content/uploads/345164
/wp-content/admin.php332142
/adminfuns.php315170
/about.php315206
/ops.php311225
/1.php304218
/mac.php295211
/155.php293193

Look at the first two rows. hellopress/wp_filemanager.php and this_is_a_new_hello_world.php were each requested by exactly 491 addresses. That is not coincidence and it is not a botnet coordinating in real time โ€” it is 491 machines running the same wordlist, from the same tool, distributed as a package.

Neither filename is a vulnerability. hellopress/wp_filemanager.php is a file manager dropped by a specific WordPress compromise kit. this_is_a_new_hello_world.php is a marker file โ€” a shell writes it to prove it can write. /x.php, /1.php, /222.php, /mac.php, /k.php, /w.php are the throwaway names an operator gives a shell they have just planted.

Every one of these requests is asking the same question: has somebody already broken into this box, and did they leave the door open behind them?

There is a second signature in the same population, and it is more precise:

Requests matching .php?p= with an empty value1,257
Distinct addresses278
Distinct paths75

The estate's own classifier documents this one well: a real CMS pagination link carries a page number. ?p= with nothing after it is a password parameter submitted blank โ€” the check for a webshell whose author never set one. The filenames are randomised (/wp-ws68.php?p=, /green3.php?p=, /qterm.php?p=, /ccc.php?p=, /ty.php?p=, /FAQ.php?p=), so no name list can enumerate them. The empty value is the tell, not the name.

This is scavenging. These 693 addresses are not attacking this estate; they are auditing it for the residue of an attack by someone else. It is cheaper to inherit access than to earn it.

What they got, mostly

Effective statusRequests
40428,479
40118,234
4036,486
3011,187
2001,037

Two observations before the third one, which is the letter.

The 18,234 ร— 401 is Letter I's mechanism running in reverse. These scanners hit protected hosts, Authelia bounced them to the portal, and a third of this population's traffic is recorded as authentication-surface activity it never intended. The two findings are the same artefact seen from opposite ends.

The 28,479 ร— 404 is the system working. A request for a shell that was never planted, on a host that runs no PHP, correctly answered "no."

And then there are the 200s.

The window

Two hosts account for essentially all of them.

Host200 on invented pathsDistinct pathsAddressesBody size
lsn-ragflow.shuffle-on.com1,093678485,135 bytes
lsn-ai.shuffle-on.com741347502,870 bytes
Combined1,8341,01598โ€”

Note the body size column. Every single one of those 1,093 RAGFlow responses was exactly 5,135 bytes. Every one of the 741 OpenWebUI responses was exactly 2,870 bytes. Six hundred and seventy-eight different filenames, one identical response.

That is a single-page application's catch-all route. Both services are SPAs: the front end owns routing, so the server returns index.html for anything it does not recognise and lets JavaScript decide what to render. For a human with a browser this is correct behaviour. For a scanner it is a confession.

The proof that the status code is arbitrary rather than meaningful:

Same identical body, served asRequestsDistinct paths
HTTP 2001,8341,015
HTTP 404915445

The same bytes, for the same class of nonexistent path, returned as success 1,834 times and as not-found 915 times. Nothing distinguishes the two groups except which code the application happened to attach. A file that does not exist was called present twice as often as it was called absent.

What did not happen

This is the part that has to be stated precisely, because the temptation to write the dramatic version is real and the dramatic version is false.

No data was exposed. The application-level authentication held throughout the window. The endpoints that would return anything of value were refused:

EndpointResultRequestsAddresses
/v1/user/tenant_info40154
/v1/user/info40144

The only endpoints that returned a genuine 200 with genuine content were the ones designed to be public, and their body sizes give them away as trivial:

EndpointResultBody
/v1/system/config20060 bytes
/v1/user/login/channels20041 bytes
/api/config200324 bytes
/api/version20038 bytes

Sixty bytes is not a document corpus. It is the configuration a login page needs before anyone has logged in.

Nobody tried to use the shells we appeared to confirm. Every one of the 1,834 false-positive responses was answered to a GET. There were no POSTs to any of those paths. The repeat-visit analysis shows only same-day doubles โ€” no address returned days later to exploit a hit it had recorded. Whatever these scanners do with their results, none of them acted on this estate inside the observation window.

So the accurate statement is narrow: this estate advertised 1,015 files it did not have, to 98 addresses, for five weeks, and nothing came of it.

But the edge was open

The false positives are the symptom. The condition underneath them is that both services were reachable from the internet without authentication at all.

The RAGFlow log makes the shape of it plain. Beyond the 1,093 catch-all responses, the host served 1,938 requests for real application resources โ€” and not to scanners:

ResourceRequestsAddressesSize
/ (application root)75605,135 bytes
/entry/js/index-6OPbUKs2.js1613319,952 bytes
/iconfont.js171380,748 bytes
/logo.svg16133,042 bytes
/chunk/js/utils-DZ66G3C6.js129100,737 bytes

Sixty distinct addresses loaded the application root. Thirteen went on to pull the entire JavaScript bundle โ€” 320 KB of it, plus every chunk. Downloading a full SPA bundle is what a browser does when a person opens the page, or what a thorough crawler does when it decides a site is worth indexing properly. Either way, thirteen strangers had this estate's RAGFlow interface rendered in front of them.

They could not log in. But they could see it existed, see what it was, and see its version.

The remediation is visible in the same data, and it is abrupt:

Date200302/401
2026-07-081400
2026-07-091620
2026-07-10252
2026-07-1104
2026-07-130167
2026-07-15025

On 2026-07-10 both hosts were placed behind Authelia forward-auth, and the 200s stop dead. lsn-ai.shuffle-on.com shows the identical cutover on the same day. Verified live while writing this letter: an invented path on either host now returns a 302 to the authentication portal.

$ curl -sI https://lsn-ragflow.shuffle-on.com/definitely-not-real-9z8q.php
HTTP/2 302
location: https://lsn-auth.shuffle-on.com/?rd=...&rm=GET

The window was closed a month before this letter was written, by the ordinary work of putting services behind the portal. It was not closed because anyone noticed this.

The instrument missed it, again

This pipeline has a mask-correction pass. It exists because of the problem documented in Letters E and G: services on this estate return HTTP 200 for things that are not successes, and counting raw status codes therefore overstates what attackers achieved. The requests table carries three columns for it โ€” masked, mask_source, and effective_status โ€” and Letter I's central claim survived only because that pass correctly rewrote a login failure from the 200 Authelia reported to the 401 it actually was.

Applied to the 1,834 false-positive responses in this letter:

Flagged masked0
mask_sourcenone
effective_status200 โ€” unchanged

Not one. The correction pass caught none of them, and the estate's own authoritative status column records 1,834 successes that were not successes.

The reason is a boundary, and it is worth naming precisely because it is the kind of gap that looks like coverage. The mask-correction understands reverse-proxy-level masking: Traefik or an error-page handler returning 200 where the origin said otherwise. It has no model of application-level masking, where the origin itself decides that an unrouted path deserves an index page and a success code. From the proxy's position those two are indistinguishable โ€” a 200 came back with a body, and it was a genuine 200 from the application's point of view. The application is simply wrong about what "success" means.

So the estate's instrument has now been caught mis-measuring the same phenomenon three times, at three different layers:

LetterWhat was trustedWhat it actually was
EX-Forwarded-ForA header the client forged
Irm=POST on the auth hostA parameter the defence wrote
JHTTP 200 from an SPAA router's shrug

The pattern is identical in all three: a value that describes a request was read as though it were the request. It is the single most reliable way this pipeline gets things wrong, and finding it a third time suggests the fix is not another special case but a standing question โ€” who authored this value, and what were they in a position to know?

What this cost, honestly

Nothing measurable, and that is not the same as nothing.

What it produced is reputational data about this estate, held by other people. Ninety-eight addresses recorded that shuffle-on.com returns 200 for /gcp-credentials.json. Some of those addresses belong to scanning operations that sell or publish their results; Letter H established that at least one verified crawler was ingesting this estate's 404s as though they were pages. A false positive does not stay local. It becomes a row in somebody else's database saying this host has an exposed service-account key, and that row does not get corrected when the underlying condition is fixed.

The likely practical consequence is more traffic, not less: a host that answers 200 to credential filenames is a host worth revisiting. It is a plausible partial explanation for why credential_harvest is the largest intent against this estate at 1,684 addresses, though the corpus cannot separate that from the internet-wide baseline, and I am not going to claim it does.

The unlikely-but-real consequence is that a targeted operator, seeing 54 different credential and service-account paths return 200 on one host, concludes the box is already compromised and worth the effort of a closer look. That did not happen here. It is the scenario the behaviour invites.

What changes here

Make the SPA catch-all return 404. Both applications should answer an unrouted path with a 404 status while still serving the SPA shell โ€” the status code and the body are independent, and every SPA framework supports this. This is the actual fix and it is small.

Teach the mask-correction about application-level masking. A response is a candidate mask when the body size is identical across many distinct paths on the same host. That heuristic would have caught all 1,834 of these: 678 paths, one byte count. Implement it as body-size entropy per host โ€” where a host serves one size for a large number of distinct paths, that size is a catch-all and every 200 carrying it is a 404. This generalises past the two hosts here.

Stop treating a 200 as an achievement in reporting. Any dashboard, ban heuristic, or dossier figure that counts "successful" requests should be recomputed against the corrected status once the above lands. The 1,037 successes attributed to the shell_upload population are almost entirely this artefact.

Audit for other unauthenticated hosts before a scanner does it for us. The two found here were closed on 2026-07-10 as routine work rather than in response to a finding, which means the estate did not know it had been open. The census can answer this directly: any host in the corpus serving 200s to invented paths is either public by design or exposed by accident, and the difference is a list somebody has to hold.

Leave the scavengers alone. The 693 addresses hunting other people's webshells are not a threat to a host with no PHP. They are a free, continuous audit of whether this estate looks compromised from outside โ€” and, as it turns out, for five weeks it did.

Method and limits

Single source: the Traefik edge access log, 2026-06-03 โ†’ 2026-08-11, 286,640 requests from 26,846 distinct addresses. That corpus is live and still ingesting, so the totals here are a snapshot taken while writing; they drift upward by tens of requests an hour and every figure was re-verified against the same snapshot after the argument was drafted. Intent classifications are this estate's own probe_intent labels โ€” and Letter I established those are not always trustworthy, so nothing here rests on a label alone. Every claim about what a host returned is taken from the recorded status code, body size, and path rather than from the classification attached to the requester.

Two live requests were made while writing, both against this estate's own hosts, to confirm the current behaviour differs from the recorded window. No third-party address described in this letter was contacted, probed, or fetched.

The exposure window is stated as 2026-06-06 โ†’ 2026-07-08 because those are the first and last dates on which a false-positive 200 appears in the corpus. The corpus itself begins 2026-06-03, so the true start is unknown and may be considerably earlier โ€” the condition is a default of both applications, not something that was switched on. The end is firmer: the transition to 302/401 on 2026-07-10 is abrupt and total across both hosts.

"Nothing leaked" is a statement about the observation window and about what the edge log can see. It is supported by the 401s on the authenticated endpoints, the absence of any POST to a false-positive path, and the absence of repeat visits. It is not supported by application-level audit logs, which were not consulted for this letter, and it cannot exclude activity before 2026-06-03.

The count of thirteen addresses that downloaded the full JavaScript bundle is a floor, not a total โ€” it counts addresses that requested the main entry chunk within the corpus window.

Confidence: HIGH on the false-positive mechanism and its scale โ€” 1,015 distinct paths returning one identical body is not open to interpretation. Confidence: HIGH on the remediation date. Confidence: HIGH that the mask-correction pass flagged none of it, which is a direct read of the stored columns. Confidence: MEDIUM on the exposure window's start date, for the reason above. Confidence: LOW on the suggestion that advertising credential filenames measurably increased traffic to this estate; it is plausible, the corpus cannot separate it from baseline, and it is offered as a hypothesis rather than a finding.

The Census, Letter J. Previous: Letter I โ€” Nobody Knocked. The series counts what arrives at one small estate's edge and what the counting gets wrong.

โš  Personal capacity. Research published independently โ€” not reflecting employer views. Derived from passive observation of attacks against personal infrastructure. Full disclaimer โ†’
โ† Previous The Census โ€” 10 / 14 Next โ†’