Niffler scans NFS servers for credentials, secrets, and misconfigurations. Think Snaffler, but for NFS instead of SMB.
NFS (especially v3) uses AUTH_SYS authentication, which sends UID/GID values in plaintext and the server blindly trusts them. There's no password, no Kerberos ticket, no challenge-response. If you can reach the NFS port, you can claim to be any non-root user on the system and read their files, or even root if no_root_squash is set.
Niffler automates the tedious parts: discovering exports, walking directory trees, spoofing UIDs, and pattern-matching file content against a library of credential signatures.
Option 1 — Download a prebuilt binary from the Releases page.
Option 2 — Install via cargo (requires libnfs-dev):
cargo install --git https://github.com/dejisec/nifflerOption 3 — Build from source (requires libnfs-dev):
git clone https://github.com/dejisec/niffler && cd niffler
cargo build --release
cp target/release/niffler .# Scan a single NFS server
./niffler scan -t 10.0.0.5
# Scan a subnet
./niffler scan -t 192.168.1.0/24
# Just recon — list servers, exports, and misconfigs without touching files
./niffler scan -t 10.0.0.0/24 --mode recon
# Browse results in the web dashboard
./niffler serve --db niffler.db
# Export findings as JSON
./niffler export --db niffler.db -f jsonNiffler runs in three modes, so you can dial in how deep you want to go:
| Mode | What it does | Use when... |
|---|---|---|
recon |
Finds NFS servers, lists exports, checks for misconfigurations | You want a quiet lay of the land |
enum |
Above + walks directory trees, matches filenames against rules | You want to see what's there without reading file content |
scan |
Above + reads file content and applies regex patterns | You want the full picture (default) |
./niffler scan -t 10.0.0.0/24 -m recon # Discovery only
./niffler scan -t 10.0.0.0/24 -m enum # Discovery + tree walk
./niffler scan -t 10.0.0.0/24 # Full scan (default)# Scan and only show high-severity findings (Red and Black)
./niffler scan -t nfs-server.internal -b red
# Scan with live console output alongside database write
./niffler scan -t nfs-server.internal --live
# Scan as a specific user (e.g., UID 1000 found during recon)
./niffler scan -t nfs-server.internal --uid 1000 --gid 1000
# Scan through a SOCKS5 proxy
./niffler scan -t 10.0.0.5 --proxy socks5://127.0.0.1:1080
# Scan local/mounted NFS shares directly (no network discovery)
./niffler scan -i /mnt/nfs_share1 /mnt/nfs_share2
# Read targets from a file (one per line, supports CIDR)
./niffler scan -T targets.txt
# Read targets from stdin
cat targets.txt | ./niffler scan -T -
# Check for subtree_check bypass (filehandle escape from export boundary)
./niffler scan -t 192.168.0.0/16 --check-subtree-bypass
# Write results to a custom database path
./niffler scan -t 10.0.0.0/24 -o engagement.db
# Generate a config template, tweak it, and reuse
./niffler scan -z > niffler.toml
# (edit niffler.toml to taste)
./niffler scan -c niffler.toml -t 10.0.0.0/24
# Launch the web dashboard to review findings
./niffler serve --db niffler.db
./niffler serve --db niffler.db --port 9090 --bind 0.0.0.0
# Export findings from the database
./niffler export --db niffler.db -f json
./niffler export --db niffler.db -f csv -b red
./niffler export --db niffler.db -f tsv --host 10.0.0.5 --scan-id 3All scan results are written to a SQLite database (niffler.db by default).
Add --live to see findings in the terminal as they're discovered, alongside the database write:
[2026-03-17 14:23:01] [BLACK] [SshPrivateKeys] [RW-] nfs-server:/exports/home/user1/.ssh/id_rsa (1.7 KB, uid:1001, gid:1001, 2025-11-03)
[2026-03-17 14:23:02] [RED] [CredentialPatterns] [RW-] nfs-server:/exports/app/.env (423 B, uid:1000, gid:1000, 2026-01-15)
Context: "DB_PASSWORD=s3cretP@ss123"
[2026-03-17 14:23:03] [RED] [AwsAccessKeys] [RW-] nfs-server:/exports/home/deploy/.aws/credentials (240 B, uid:1002, gid:1002, 2025-09-22)
Context: "aws_access_key_id = AKIAIOSFODNN7EXAMPLE"
Launch a local web UI for interactive triage — filter, star, and review findings in your browser:
./niffler serve --db niffler.db
# Open http://127.0.0.1:8080Export findings from the database as JSON lines, CSV, or TSV:
./niffler export --db niffler.db -f json # JSON lines to stdout
./niffler export --db niffler.db -f csv -b red # CSV, Red and Black only
./niffler export --db niffler.db -f tsv --host 10.0.0.5 # TSV, single hostFindings are triaged into four levels. Use -b to set a minimum severity threshold.
| Level | Meaning | Examples |
|---|---|---|
| Black | Immediate, direct impact — usable credentials or key material | SSH private keys, shadow files, Vault tokens, KeePass databases |
| Red | High-value — likely contains secrets, needs a closer look | .env files with passwords, AWS access keys, database connection strings |
| Yellow | Interesting — worth noting but may not be directly exploitable | Config files, log files with potential info |
| Green | Informational — context that helps paint the bigger picture | Scripts, documentation, data files on interesting exports |
./niffler scan -t 10.0.0.5 -b red # Only Red and Black findings
./niffler scan -t 10.0.0.5 -b black # Only Black findingsNiffler runs as a multi-phase async pipeline:
Targets ──► Discovery ──► Tree Walker ──► File Scanner ──► Output
│ │ │
find servers walk exports read content
list exports parallel dirs match filenames
harvest UIDs prune junk dirs match patterns
detect misconfig retry on loss check for keys
UID cycling
retry on loss
Discovery finds NFS servers (port scan on 111/2049), queries the portmapper and MOUNT service for exports, harvests UIDs from directory listings, and checks for misconfigurations.
Tree Walker does a parallel recursive READDIRPLUS traversal of each export (--parallel-dirs concurrent directory listings), applying directory discard rules to prune uninteresting paths early.
File Scanner reads file content and runs it through the rule engine using a connection pool (--max-connections-per-host). If a file is permission-denied, Niffler automatically cycles through harvested UIDs (AUTH_SYS spoofing) to try accessing it as different users. Failed reads are retried with exponential backoff (--scan-retries).
Circuit Breaker monitors error rates per host. If a server's error rate exceeds 80% within a sliding window (after at least --error-threshold events), the host is temporarily suspended for --cooldown-secs to avoid hammering unhealthy servers.
UID Cycling is the secret sauce. When the scanner hits a permission wall, it tries:
- The primary UID (from
--uid/--gid, default: nobody/65534) - The file's owning UID (from stat — most likely to work)
- UIDs harvested during discovery (from directory listings)
Each UID attempt creates a new NFS connection with fresh AUTH_SYS credentials. NFS file handles are cross-connection valid, so a handle obtained by the walker can be read by the scanner using a completely different UID.
Niffler supports NFSv4 via libnfs. Force it with --nfs-version 4:
./niffler scan -t 10.0.0.5 --nfs-version 4When no version is specified, Niffler defaults to NFSv3. NFSv4 is useful when the target only exposes v4, or when you want to test v4-specific behaviors.
Rules are defined in TOML and compiled into the binary. The engine uses a relay-chain architecture (borrowed from Snaffler): cheap rules gate expensive ones.
For example, a file named .env first matches a filename rule (instant). That rule relays to content rules, which read the file and apply regex patterns (expensive). This way, regex only runs on files that are likely to contain something interesting.
.env file found
└─► FileEnumeration rule matches ".env" (Relay action)
├─► CredentialPatterns: scans for password=, api_key=, bearer tokens, etc.
├─► CloudKeyPatterns: scans for AKIA..., aws_secret_access_key, etc.
└─► TokenPatterns: scans for Slack xox*, GitHub ghp_, JWT, etc.
Rules have four scopes:
- ShareEnumeration — applied to export paths during discovery
- DirectoryEnumeration — applied to directory names during tree walk
- FileEnumeration — applied to filenames/extensions/paths in the scanner
- ContentsEnumeration — applied to file content (most expensive, gated by relays)
Replace the defaults entirely or merge your own rules on top:
# Replace all built-in rules with your own
./niffler scan -t 10.0.0.5 -r /path/to/my-rules/
# Keep defaults and add extra rules
./niffler scan -t 10.0.0.5 -R /path/to/extra-rules/Rules are TOML files with a straightforward structure:
[[rules]]
name = "MyCustomPattern"
scope = "ContentsEnumeration"
match_location = "FileContentAsString"
match_type = "Regex"
patterns = ['(?i)internal_api_key\s*=\s*["\'][^"\']{16,}']
action = "Snaffle"
triage = "Red"
max_size = 1048576
context_bytes = 200
description = "Custom internal API key pattern"During discovery, Niffler probes each export for common NFS misconfigurations:
| Check | What it means | How Niffler tests it |
|---|---|---|
| no_root_squash | Server trusts UID 0 — you can read/write anything as root | Connects as UID 0, attempts getattr on the export root |
| insecure | Export accepts connections from unprivileged ports (>1024) | Connects from a high port, checks if getattr succeeds |
| subtree_check bypass | File handles can escape the export boundary | Looks up .. from the export root, checks if the returned handle differs |
# Enable subtree bypass check (off by default, adds extra probe per export)
./niffler scan -t 10.0.0.0/24 --check-subtree-bypass| Flag | Description | Default |
|---|---|---|
-v, --verbosity |
Log level: trace, debug, info, warn, error |
info |
| Flag | Description | Default |
|---|---|---|
-t, --targets |
IP addresses, hostnames, or CIDR ranges | — |
-T, --target-file |
Read targets from file (one per line, - for stdin) |
— |
-i, --local-path |
Scan local/mounted paths instead of NFS discovery | — |
| Flag | Description | Default |
|---|---|---|
-m, --mode |
Operating mode: recon, enum, scan |
scan |
-o, --output |
SQLite database path for results | niffler.db |
-l, --live |
Print findings to terminal alongside database write | false |
-b, --min-severity |
Minimum triage level: green, yellow, red, black |
green |
| Flag | Description | Default |
|---|---|---|
--uid |
UID for AUTH_SYS credentials | 65534 (nobody) |
--gid |
GID for AUTH_SYS credentials | 65534 (nobody) |
--no-uid-cycle |
Disable auto-cycling through harvested UIDs on permission denied | false |
--max-uid-attempts |
Max UID attempts per file before giving up | 5 |
--nfs-version |
Force NFS version: 3 or 4 (auto-detect if not set) |
— |
--no-privileged-port |
Disable binding source port < 1024 | false |
--proxy |
SOCKS5 proxy URL (e.g., socks5://127.0.0.1:1080) |
— |
| Flag | Description | Default |
|---|---|---|
--max-connections-per-host |
Max concurrent NFS connections per server | 8 |
--discovery-tasks |
Max concurrent discovery tasks | 30 |
--discovery-timeout |
Timeout in seconds for discovery network operations | 5 |
--walker-tasks |
Max concurrent tree walk tasks (one per export) | 20 |
--scanner-tasks |
Max concurrent file scan tasks | 50 |
--max-depth |
Max directory depth during tree walk | 50 |
--parallel-dirs |
Max concurrent directory listings per export during tree walk | 8 |
--walk-retries |
Max retries per export walk on connection loss (0 = no retry) | 2 |
--walk-retry-delay |
Base delay between walk retries (ms, exponential backoff with jitter) | 500 |
--scan-retries |
Max retries per file scan on connection loss (0 = no retry) | 2 |
--scan-retry-delay |
Base delay between scanner retries (ms, exponential backoff with jitter) | 200 |
--connect-timeout |
Timeout in seconds for establishing NFS connections | 10 |
--nfs-timeout |
Timeout in seconds for individual NFS operations (read, readdirplus) | 30 |
--task-timeout |
Timeout in seconds for entire scanner task | 300 |
--max-scan-size |
Max file size to read content from (bytes) | 1048576 (1 MB) |
--read-chunk-size |
NFS read chunk size in bytes | 1048576 (1 MB) |
--error-threshold |
Min events in health window before circuit breaker evaluates error rate | 10 |
--cooldown-secs |
Cooldown duration in seconds after circuit breaker trips | 60 |
| Flag | Description | Default |
|---|---|---|
-r, --rules-dir |
Custom rules directory (replaces defaults) | — |
-R, --extra-rules |
Additional rules directory (merged with defaults) | — |
-c, --config |
Load config from TOML file | — |
-z, --generate-config |
Print current config as TOML and exit | false |
--check-subtree-bypass |
Enable subtree_check bypass detection | false |
| Flag | Description | Default |
|---|---|---|
--db |
Path to SQLite database (required) | — |
--port |
Port to listen on | 8080 |
--bind |
Address to bind to | 127.0.0.1 |
| Flag | Description | Default |
|---|---|---|
--db |
Path to SQLite database (required) | — |
-f, --format |
Output format: json, csv, tsv (required) |
— |
-b, --min-severity |
Minimum triage level filter | — |
--host |
Filter by host | — |
--rule |
Filter by rule name | — |
--scan-id |
Filter by scan ID | — |