DevLab / security / smb-share-enumeration-tool
SECURITY PYTHON ADVANCED

SMB Share Enumeration Tool

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

Python3.10+
OSLinux / macOS
Lines~198
Packages1 deps
Install Dependencies
$ pip install impacket
Usage
$ python3 smb_enum.py --target 192.168.1.100 --user guest
#security#python#advanced#impacket

SMB Share Enumeration Tool

Problem Statement

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.

How It Works

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.).

Full Script

#!/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())

Configuration

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

Usage Examples

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

Error Handling

  • Authentication failure (STATUS_LOGON_FAILURE, bad password, locked account) raises SessionError during conn.login(); the script logs the reason and exits with code 1.
  • Network unreachable / DNS failure / timeout raises a generic exception on connect; caught and reported without a traceback, exit code 1.
  • Per-share access denial is expected and handled gracefully — the share is marked DENIED and scanning continues. A single locked-down share never aborts the run.
  • Protocol errors during probing (unusual SMB dialects, malformed responses) are caught per-share and marked ERROR with the message recorded.
  • Malformed hash string is validated up front and exits with code 2 before any network activity.
  • Missing target is validated before connecting; exit code 2.
  • Connection cleanup is guaranteed via try/finally, so the socket is always closed even if enumeration raises.

Extending Ideas

  • Recursive listing: walk into each readable share and dump the full file tree (with size and mtime) to spot juicy filenames like *.kdbx, id_rsa, or web.config.
  • Output formats: add --json / --csv flags by serializing the ShareResult dataclass list — trivial since results are already structured.
  • Parallel scanning: wrap probe_share calls in a concurrent.futures.ThreadPoolExecutor to enumerate dozens of hosts from a target list simultaneously.
  • Credential spraying mode: accept a file of user:password pairs and iterate, reporting which combinations authenticate — useful for auditing weak service accounts.
  • Integration with BloodHound/Neo4j: push writable-share findings into a graph so testers can pivot from "share I can write to" to "host that mounts it."
// RELATED_SCRIPTS
← Back to DevLab valtersit.com