smb-anonymous-access can never match, and the obvious fix makes it a false positive on any guest-mapping server
Summary
Two linked problems in the JS SMB stack, found while investigating a customer-reported false positive. They are filed together because fixing the first without the second makes things worse for users, and the second needs a library primitive that does not exist yet.
smb-anonymous-accesscan never match, on any server, on current builds.- The obvious one-line fix turns it into a false positive on every guest-mapping SMB server — which is most NAS appliances.
Tested on nuclei v3.11.1, template javascript/misconfiguration/smb/smb-anonymous-access.yaml, against real Samba servers.
Part 1 — the template can never fire
The template asks for an anonymous session the only way a template can:
args:
Host: "{{Host}}"
Port: "445"
User: " "
Pass: " "That single space is trimmed to an empty username before it reaches the wire. In pkg/js/libs/smbsession/path.go, ParseIdentity opens with user = strings.TrimSpace(user) and returns "", "" for a blank string; session.go then falls back to strings.TrimSpace(creds.User), which is empty again:
domain, user := ParseIdentity(creds.User)
if creds.Domain != "" { domain = creds.Domain }
if user == "" { user = strings.TrimSpace(creds.User) }goimpacket then refuses client-side, before anything is sent:
SMB login failed: internal error: Anonymous account is not supported yet. Use guest account insteadMeasured against two Samba servers by calling smbsession.Dial directly:
User value |
result |
|---|---|
" " (as shipped) |
dial fails — "Anonymous account is not supported yet" |
"" |
dial fails — same |
"guest" |
succeeds, enumerates shares |
Deterministic and server-independent: it is a client-side refusal, so the template yields nothing anywhere. A scan of five differently-configured Samba hosts produced zero matches for this template.
Part 2 — why User: "guest" is not the fix
Switching the username to guest makes the template run, and then it reports almost everything. Four Samba servers, all enumerated as guest:
| server | shares guest can enumerate | shipped matcher contains(response,"IPC$") |
matcher ignoring admin shares | should report? |
|---|---|---|---|---|
map to guest = Bad User, shares restricted to a real user |
[Backups IPC$ Media] |
fires | fires | no |
world-readable Public |
[IPC$ Public] |
fires | fires | yes |
| printer, only spooler shares | [IPC$ direct hold lp1 print] |
fires | fires | no |
| no shares published | [IPC$] |
fires | no | no |
Two things are going on:
contains(response, "IPC$")cannot discriminate at all. IPC$ is the pipe the enumeration itself travelled over — every successfulsrvsvc.NetShareEnumAllreturns it. Deleting the last real share on a server does not clear the finding.- Tightening the matcher to require a non-administrative share only rescues the last row. On the first and third servers the guest session can read the share names but cannot open any of them —
TREE_CONNECTreturnsSTATUS_ACCESS_DENIED.map to guest = Bad Useris the default on a lot of NAS and backup appliances, so this is the common case, not a corner one.
The underlying point: share-name enumeration is not access, and no matcher over a name list can tell the two apart. A template-only fix trades a dead check for a noisy one.
Related: the same gap in smb-default-login
javascript/default-logins/smb-default-login.yaml matches on response != "[]" and success == true. Because that template does not call Export, response reaches the dsl as the value from results.Export() — a Go []string, compared against a string literal, so the clause is true for every slice and never discriminates. The effective gate is success == true alone, which any guest-mapping server satisfies for all nine credential pairs. Same root cause: the check rests on "the enumeration succeeded" rather than "something was reachable".
Suggested direction (your design call)
A correct check needs to demonstrate reachability, not enumeration. That is a library capability rather than a matcher one, so it would be a two-part change:
pkg/js/libs/smb— expose something likeListReachableShares(host, port, user, pass)that enumerates, drops administrative shares (IPC$, ADMIN$, C$..Z$, print$) and, for the remainder, performs aTREE_CONNECTand a root listing, returning only the shares that actually opened.smbsessionalready has everything needed —Session.ListDirdoes mount-then-list today.- the template — use a real username (
guest) and match on that reachable list instead of oncontains(response, "IPC$").
Optionally, a session that authenticates a random 20-character username is a reliable tell that the server validates nothing, which is what separates "these credentials work" from "this server accepts anything" for smb-default-login.
We have implemented and measured this approach downstream and would be glad to prepare PRs for both repos if you think the direction is right — or to just fix the username if you would rather take the smaller change and accept the false positives. Happy to follow whatever you prefer.
Reproducing
Two Samba configs are enough to see both halves:
# fires, and should: Public is genuinely world-readable
[global]
map to guest = Bad User
[Public]
path = /srv/Public
guest ok = yes
read only = yes# fires once the username is fixed, and should NOT: guest can read the
# share names and open neither
[global]
map to guest = Bad User
[Backups]
path = /srv/Backups
valid users = realuser
guest ok = nosmbclient -L //<host> -U 'RandomUser20Chars%whatever' lists the shares on both; smbclient //<host>/Backups -U 'RandomUser20Chars%whatever' -c ls returns NT_STATUS_ACCESS_DENIED on the second.
Source: projectdiscovery/nuclei