During internal penetration tests and security audits, one of the first things a tester needs to understand is what SMB shares are exposed on a target host
During internal penetration tests and security audits, one of the first things a tester needs to understand is what SMB shares are exposed on a target host and what level of access the current credentials provide. Misconfigured shares frequently leak sensitive data — backups, config files, source code, credentials — and are a common initial foothold in real-world intrusions. Manually checking each share with smbclient or net view is tedious and error-prone, especially across many hosts.
This script automates the process: it authenticates to a target over SMB, enumerates every share the server advertises, and then probes each one to determine whether the supplied credentials can read, write, or only list it. The result is a concise, structured report that a tester can immediately act on, and that a defender can use to spot overly permissive shares.
The script is built on top of the impacket library, specifically SMBConnection for the low-level SMB protocol work and NetBIOS/SMB helpers for name resolution and share listing. The flow is: (1) parse credentials and target from CLI/env, (2) establish an SMB session with NTLM authentication (supporting both password and pass-the-hash), (3) call listShares() to get the server's advertised share list, (4) for each share, attempt to connect and list the root directory to classify access as READ, WRITE, or DENIED, and (5) print a formatted summary.
Design choices: credentials are pulled from environment variables with CLI overrides so the tool can be used non-interactively in CI or from a wrapper script without leaking secrets into shell history; every network operation is wrapped so a single failing share doesn't abort the whole scan; and results are accumulated into a dataclass so the output layer is trivially swappable (JSON, CSV, etc.).
#!/usr/bin/env python3
"""
SMB Share Enumeration Tool
Authenticates to a target SMB server, enumerates advertised shares, and
classifies the access level granted to the supplied credentials.
Usage:
python3 smb_enum.py --target 192.168.1.100 --user guest
Environment variables (used as defaults, CLI args override):
SMB_TARGET, SMB_USER, SMB_PASSWORD, SMB_DOMAIN, SMB_HASH, SMB_TIMEOUT
"""
from __future__ import annotations
import argparse
import logging
import os
import sys
from dataclasses import dataclass, field
from typing import Optional
from impacket.smbconnection import SMBConnection, SessionError
# ---------------------------------------------------------------------------
# Configuration (env-var driven with sensible defaults)
# ---------------------------------------------------------------------------
DEFAULT_TARGET: str = os.environ.get("SMB_TARGET", "")
DEFAULT_USER: str = os.environ.get("SMB_USER", "guest")
DEFAULT_PASSWORD: str = os.environ.get("SMB_PASSWORD", "")
DEFAULT_DOMAIN: str = os.environ.get("SMB_DOMAIN", "")
DEFAULT_HASH: str = os.environ.get("SMB_HASH", "") # LM:NT for pass-the-hash
DEFAULT_TIMEOUT: int = int(os.environ.get("SMB_TIMEOUT", "10"))
LOG = logging.getLogger("smb_enum")
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
@dataclass
class ShareResult:
"""Represents the enumeration result for a single share."""
name: str
remark: str = ""
access: str = "UNKNOWN" # READ | WRITE | DENIED | ERROR
detail: str = "" # error message or extra info
entries: list[str] = field(default_factory=list)
# ---------------------------------------------------------------------------
# Core logic
# ---------------------------------------------------------------------------
def build_connection(
target: str,
user: str,
password: str,
domain: str,
lmhash: str,
nthash: str,
timeout: int,
) -> SMBConnection:
"""Create and authenticate an SMBConnection to the target."""
LOG.debug("Connecting to %s (user=%r domain=%r)", target, user, domain)
conn = SMBConnection(
remoteName=target,
remoteHost=target,
timeout=timeout,
)
# If a hash was supplied, use pass-the-hash; otherwise plain password.
if lmhash or nthash:
conn.login(user, password, domain, lmhash=lmhash, nthash=nthash)
else:
conn.login(user, password, domain)
LOG.info("Authenticated to %s as %s\\%s", target, domain or ".", user)
return conn
def parse_hash(raw: str) -> tuple[str, str]:
"""Split an 'LMHASH:NTHASH' string into two halves."""
if not raw:
return "", ""
parts = raw.split(":", 1)
if len(parts) != 2:
raise ValueError("SMB_HASH must be in LMHASH:NTHASH format")
return parts[0], parts[1]
def list_shares(conn: SMBConnection) -> list[ShareResult]:
"""Enumerate the shares the server advertises."""
results: list[ShareResult] = []
try:
shares = conn.listShares()
except SessionError as exc:
LOG.error("Failed to list shares: %s", exc)
raise
for share in shares:
name = share["shi1_netname"][:-1] # strip trailing NUL
remark = share["shi1_remark"][:-1]
LOG.debug("Discovered share: %s (%s)", name, remark)
results.append(ShareResult(name=name, remark=remark))
return results
def probe_share(conn: SMBConnection, share: ShareResult) -> None:
"""Attempt to list and write to a share, updating its access level."""
# Step 1: try to list contents — this proves at least READ access.
try:
entries = conn.listPath(share.name, "*")
share.entries = [e.get_longname() for e in entries if e.get_longname() not in (".", "..")]
share.access = "READ"
except SessionError as exc:
share.access = "DENIED"
share.detail = str(exc)
LOG.warning("Access denied to %s: %s", share.name, exc)
return
except Exception as exc: # noqa: BLE001 - protocol errors vary widely
share.access = "ERROR"
share.detail = str(exc)
LOG.warning("Error probing %s: %s", share.name, exc)
return
# Step 2: attempt a benign write to detect WRITE access.
probe_name = ".smb_enum_write_test"
try:
conn.putFile(share.name, probe_name, lambda: b"")
conn.deleteFile(share.name, probe_name)
share.access = "WRITE"
except SessionError:
# Read-only is the expected outcome on a well-configured share.
pass
except Exception as exc: # noqa: BLE001
share.detail = f"write probe failed: {exc}"
def print_report(target: str, user: str, results: list[ShareResult]) -> None:
"""Print a human-readable summary of the enumeration."""
print(f"\n=== SMB Share Enumeration: {target} (user={user}) ===\n")
if not results:
print("No shares returned by the server.")
return
width = max(len(r.name) for r in results) + 2
print(f"{'SHARE'.ljust(width)}ACCESS REMARK")
print("-" * (width + 40))
for r in results:
print(f"{r.name.ljust(width)}{r.access.ljust(9)}{r.remark}")
if r.access == "WRITE":
print(f"{' ' * width} [!] WRITABLE — potential data integrity risk")
if r.detail and r.access in ("DENIED", "ERROR"):
print(f"{' ' * width} ({r.detail})")
writable = [r.name for r in results if r.access == "WRITE"]
if writable:
print(f"\nWritable shares: {', '.join(writable)}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Enumerate SMB shares and classify credential access.",
)
parser.add_argument("--target", default=DEFAULT_TARGET, help="Target host/IP")
parser.add_argument("--user", default=DEFAULT_USER, help="Username")
parser.add_argument("--password", default=DEFAULT_PASSWORD, help="Password")
parser.add_argument("--domain", default=DEFAULT_DOMAIN, help="AD domain (optional)")
parser.add_argument(
"--hash",
default=DEFAULT_HASH,
help="LMHASH:NTHASH for pass-the-hash authentication",
)
parser.add_argument(
"--timeout", type=int, default=DEFAULT_TIMEOUT, help="Socket timeout (s)"
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable debug logging"
)
return parser.parse_args(argv)
def main(argv: Optional[list[str]] = None) -> int:
args = parse_args(argv)
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s: %(message)s",
)
if not args.target:
LOG.error("No target specified. Use --target or set SMB_TARGET.")
return 2
try:
lmhash, nthash = parse_hash(args.hash)
except ValueError as exc:
LOG.error("%s", exc)
return 2
try:
conn = build_connection(
args.target, args.user, args.password, args.domain,
lmhash, nthash, args.timeout,
)
except SessionError as exc:
LOG.error("Authentication failed: %s", exc)
return 1
except Exception as exc: # noqa: BLE001 - network errors are varied
LOG.error("Could not connect to %s: %s", args.target, exc)
return 1
try:
results = list_shares(conn)
for share in results:
# Skip IPC$ — it is not enumerable as a filesystem share.
if share.name.upper() == "IPC$":
share.access = "N/A"
continue
probe_share(conn, share)
finally:
try:
conn.close()
except Exception: # noqa: BLE001
pass
print_report(args.target, args.user, results)
return 0
if __name__ == "__main__":
sys.exit(main())
All configuration can be supplied either via environment variables (useful for CI, wrappers, or avoiding shell history leakage) or via CLI flags, which take precedence.
| Variable / Flag | Purpose | Default |
|---|---|---|
SMB_TARGET / --target |
Target hostname or IP address. Required. | (empty) |
SMB_USER / --user |
Username to authenticate with. | guest |
SMB_PASSWORD / --password |
Password for the user. | (empty) |
SMB_DOMAIN / --domain |
AD domain or workgroup. | (empty) |
SMB_HASH / --hash |
LMHASH:NTHASH for pass-the-hash. Overrides password. |
(empty) |
SMB_TIMEOUT / --timeout |
Socket timeout in seconds. | 10 |
--verbose |
Enable debug logging (share discovery details). | off |
1. Anonymous/guest enumeration against a lab host:
$ python3 smb_enum.py --target 192.168.1.100 --user guest
INFO: Authenticated to 192.168.1.100 as .\guest
=== SMB Share Enumeration: 192.168.1.100 (user=guest) ===
SHARE ACCESS REMARK
----------------------------------------
ADMIN$ DENIED Remote Admin
C$ DENIED Default share
IPC$ N/A Remote IPC
Public READ Public file drop
Backups WRITE Nightly backups
[!] WRITABLE — potential data integrity risk
Writable shares: Backups
2. Authenticated enumeration with a domain account:
$ python3 smb_enum.py --target fileserver.corp.local \
--user jdoe --password 'S3cret!' --domain CORP
INFO: Authenticated to fileserver.corp.local as CORP\jdoe
...
3. Pass-the-hash (no plaintext password):
$ export SMB_HASH="aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0"
$ python3 smb_enum.py --target 10.0.0.42 --user Administrator --domain CORP
STATUS_LOGON_FAILURE, bad password, locked account) raises SessionError during conn.login(); the script logs the reason and exits with code 1.1.DENIED and scanning continues. A single locked-down share never aborts the run.ERROR with the message recorded.2 before any network activity.2.try/finally, so the socket is always closed even if enumeration raises.*.kdbx, id_rsa, or web.config.--json / --csv flags by serializing the ShareResult dataclass list — trivial since results are already structured.probe_share calls in a concurrent.futures.ThreadPoolExecutor to enumerate dozens of hosts from a target list simultaneously.user:password pairs and iterate, reporting which combinations authenticate — useful for auditing weak service accounts.