diff --git a/.claude/plans/integration_test_ci.md b/.claude/plans/integration_test_ci.md new file mode 100644 index 0000000..e9f2c43 --- /dev/null +++ b/.claude/plans/integration_test_ci.md @@ -0,0 +1,205 @@ +# GitHub Actions CI: Integration Tests with Samba AD DC + SQL Server + +## Context + +MSSQLHound needs CI that validates all expected edges are created against a real AD environment. The integration test framework already handles AD object creation via LDAP, SQL setup/teardown, collector execution, and edge validation — we just need to provide the infrastructure. + +**Architecture**: SQL Server installed on the runner host (via apt) + Samba AD DC in a Docker container. SQL Server is configured for AD auth via a keytab from `samba-tool`. The integration test framework (`TestIntegrationAll`) handles everything else. + +## Network and domain config + +| Setting | Value | +|---------|-------| +| Subnet | `10.2.0.0/20` | +| DC IP | `10.2.10.100` | +| SQL Server | On the runner host (localhost) | +| Domain FQDN | `MAYYHEM.COM` | +| NetBIOS | `MAYYHEM` | +| Admin account | `MAYYHEM\domainadmin` (sysadmin on SQL) | +| Password (all) | `password` | +| SQL auth mode | Mixed mode (Windows + SQL) | + +## Files to create + +- `.github/workflows/ci.yml` (new) + +## Plan + +### 1. Create `.github/workflows/ci.yml` with two jobs + +**Job 1: `unit-tests`** — Fast validation of all edge creation logic +- `ubuntu-latest`, checkout, setup Go, `go test -v -count=1 ./...` + +**Job 2: `integration-tests`** — Full pipeline against real AD + SQL Server on `ubuntu-22.04` + +#### Step 1: Checkout + Setup Go + +#### Step 2: Start Samba AD DC +```bash +docker network create --subnet=10.2.0.0/20 adnet + +docker run -d --privileged \ + --name dc --hostname DC \ + --network adnet --ip 10.2.10.100 \ + -e REALM='MAYYHEM.COM' \ + -e DOMAIN='MAYYHEM' \ + -e ADMIN_PASS='password' \ + -e DNS_FORWARDER='8.8.8.8' \ + -p 389:389/tcp -p 389:389/udp \ + -p 636:636/tcp \ + -p 88:88/tcp -p 88:88/udp \ + -p 464:464/tcp -p 464:464/udp \ + diegogslomp/samba-ad-dc +``` + +#### Step 3: Wait for Samba DC readiness +Poll `samba-tool domain info` up to 60 attempts / 2s. + +#### Step 4: Create domainadmin user + SQL Server service account + keytab +```bash +# Create domainadmin user +docker exec dc samba-tool user create domainadmin 'password' --use-username-as-cn +docker exec dc samba-tool group addmembers "Domain Admins" domainadmin + +# Create SQL Server service account +docker exec dc samba-tool user create sqlsvc 'password' --use-username-as-cn +HOSTNAME=$(hostname) +docker exec dc samba-tool spn add MSSQLSvc/${HOSTNAME}.mayyhem.com sqlsvc +docker exec dc samba-tool spn add MSSQLSvc/${HOSTNAME}.mayyhem.com:1433 sqlsvc + +# Export keytab +docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab --principal=sqlsvc +docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab \ + --principal=MSSQLSvc/${HOSTNAME}.mayyhem.com +docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab \ + --principal=MSSQLSvc/${HOSTNAME}.mayyhem.com:1433 +docker cp dc:/tmp/mssql.keytab /tmp/mssql.keytab +``` + +#### Step 5: Configure host DNS + Kerberos +```bash +# DNS +echo "10.2.10.100 dc.mayyhem.com dc mayyhem.com" | sudo tee -a /etc/hosts +sudo sed -i '1i nameserver 10.2.10.100' /etc/resolv.conf + +# Kerberos +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y krb5-user +cat <<'EOF' | sudo tee /etc/krb5.conf +[libdefaults] + default_realm = MAYYHEM.COM + dns_lookup_realm = false + dns_lookup_kdc = false + +[realms] + MAYYHEM.COM = { + kdc = 10.2.10.100 + admin_server = 10.2.10.100 + default_domain = mayyhem.com + } + +[domain_realm] + .mayyhem.com = MAYYHEM.COM + mayyhem.com = MAYYHEM.COM +EOF +``` + +#### Step 6: Install SQL Server 2022 +```bash +curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | \ + sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg +curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list | \ + sudo tee /etc/apt/sources.list.d/mssql-server-2022.list +sudo apt-get update +sudo apt-get install -y mssql-server +sudo MSSQL_SA_PASSWORD='password' MSSQL_PID='Developer' \ + /opt/mssql/bin/mssql-conf setup accept-eula +``` + +#### Step 7: Enable mixed mode auth +```bash +sudo /opt/mssql/bin/mssql-conf set sqlagent.enabled true +sudo /opt/mssql/bin/mssql-conf set network.kerberoskeytabfile /var/opt/mssql/secrets/mssql.keytab +sudo /opt/mssql/bin/mssql-conf set network.privilegedadaccount sqlsvc + +# Enable mixed mode (SQL + Windows auth) +# SQL Server Linux uses the MSSQL_SA_PASSWORD being set during setup to enable mixed mode. +# To explicitly toggle it post-setup if needed: +sudo /opt/mssql/bin/mssql-conf set sqlagent.enabled true +``` + +Note: SQL Server on Linux enables mixed mode auth when SA password is set during setup. The `MSSQL_SA_PASSWORD` in step 6 handles this. + +#### Step 8: Configure SQL Server keytab + restart +```bash +sudo mkdir -p /var/opt/mssql/secrets +sudo cp /tmp/mssql.keytab /var/opt/mssql/secrets/mssql.keytab +sudo chown mssql:mssql /var/opt/mssql/secrets/mssql.keytab +sudo chmod 400 /var/opt/mssql/secrets/mssql.keytab +sudo systemctl restart mssql-server +sleep 5 +``` + +#### Step 9: Create domainadmin as SQL sysadmin +```bash +# Install sqlcmd +curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/prod.list | \ + sudo tee /etc/apt/sources.list.d/mssql-release.list +sudo apt-get update +sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18 + +export PATH="$PATH:/opt/mssql-tools18/bin" +sqlcmd -S localhost -U sa -P 'password' -C -Q " + CREATE LOGIN [MAYYHEM\domainadmin] FROM WINDOWS; + ALTER SERVER ROLE [sysadmin] ADD MEMBER [MAYYHEM\domainadmin]; +" +``` + +#### Step 10: Verify AD auth +```bash +echo 'password' | kinit Administrator@MAYYHEM.COM +klist +``` + +#### Step 11: Run integration tests +```bash +MSSQL_SERVER=localhost \ +MSSQL_USER=sa \ +MSSQL_PASSWORD='password' \ +MSSQL_DOMAIN=mayyhem.com \ +MSSQL_DC=10.2.10.100 \ +LDAP_USER='Administrator@mayyhem.com' \ +LDAP_PASSWORD='password' \ +MSSQL_SKIP_DOMAIN=false \ +MSSQL_ACTION=all \ +MSSQL_SKIP_HTML=true \ +go test -v -count=1 -tags integration -timeout 30m \ + -run TestIntegrationAll ./internal/collector/... +``` + +Since `MSSQL_DOMAIN=mayyhem.com` → `substituteDomain()` extracts `MAYYHEM` as NetBIOS, which matches the hardcoded `MAYYHEM\` references in the SQL scripts. No substitution gap. + +## Critical files + +| File | Role | +|------|------| +| [integration_setup_test.go](internal/collector/integration_setup_test.go) | Config loading, LDAP object creation, SQL setup orchestration | +| [integration_sql_test.go](internal/collector/integration_sql_test.go) | Embedded SQL scripts with `FROM WINDOWS` + `$Domain` references | +| [edge_integration_test.go](internal/collector/edge_integration_test.go) | `TestIntegrationAll` entry point, edge validation | +| [edge_test_data_test.go](internal/collector/edge_test_data_test.go) | 200+ edge test case definitions | + +## Risks and mitigations + +| Risk | Mitigation | +|------|------------| +| Port 53 conflict on runner | Don't publish port 53; use `/etc/hosts` + `/etc/resolv.conf` | +| SQL Server 2022 not on Ubuntu 24.04 | Pin `ubuntu-22.04` | +| Samba DC slow to start | 120s timeout with polling | +| `FROM WINDOWS` fails if DNS broken | Verify `kinit` works before running tests | +| `diegogslomp/samba-ad-dc` unavailable | Fall back to `craftdock/samba-ad-dc` | +| Mixed mode not enabled | SA password set during setup enables it automatically | + +## Verification + +1. Unit tests locally: `go test -v ./...` +2. Push to branch and verify both jobs pass +3. Integration test output includes edge coverage report from `TestIntegrationAll` diff --git a/.claude/plans/logging.md b/.claude/plans/logging.md new file mode 100644 index 0000000..712aa1d --- /dev/null +++ b/.claude/plans/logging.md @@ -0,0 +1,151 @@ +# Logging Overhaul Plan + +## Context +MSSQLHound currently uses raw `fmt.Printf`/`fmt.Println` for all logging (~149 calls across 10 files). Messages have no timestamps, no log levels, and inconsistent formatting. The goal is to add UTC timestamps and log levels to every message using Go's stdlib `log/slog` (available since Go 1.21, project uses Go 1.24). + +## Approach: Use `log/slog` from stdlib + +No new packages or dependencies. Create a `*slog.Logger` in `main()`, propagate via struct fields. + +### Output format +``` +INFO 2026-03-30T14:22:01Z Processing 5 SQL Server(s)... +INFO 2026-03-30T14:22:01Z [corp.local] Enumerating MSSQL SPNs from Active Directory... +VERBOSE 2026-03-30T14:22:01Z [corp.local] Found SPNs count=12 host=sql01.corp.local +WARNING 2026-03-30T14:22:02Z [sql01.corp.local] SPN enumeration failed error="connection refused" +DEBUG 2026-03-30T14:22:02Z [sql01.corp.local] EPA TLS handshake complete cipher=0x1301 +``` + +- Custom `slog.Handler` implementation in a new file `internal/logging/handler.go` +- Format: `LEVEL TIMESTAMP [target] message attrs...` +- Level name is left-aligned, right-padded to 7 chars (longest: `WARNING` and `VERBOSE`) +- Output goes to **stderr** (standard for logs; keeps stdout clean for data output like tables) +- Timestamps in UTC RFC3339 format +- **Target context**: when processing a specific server or domain, a `[target]` tag appears after the timestamp. Implemented via `logger.With("target", serverName)` to create sub-loggers. The custom handler renders the `target` attr specially in brackets, separate from other attrs. +- Messages without a target (startup, global config) omit the bracket section + +### Colors (ANSI escape codes) +- Auto-detect TTY on stderr (`os.Stderr.Fd()` + `isatty` check or `golang.org/x/term.IsTerminal`). Disable colors when piped. +- **Level colors**: + - `ERROR` — red (ANSI 31) + - `WARNING` — yellow (ANSI 33) + - `INFO` — white/default (no color) + - `VERBOSE` — dim/gray (ANSI 90) + - `DEBUG` — magenta (ANSI 35) +- **Timestamp** — light blue (ANSI 94) +- **Target** `[brackets]` — deterministic color per unique target string. Hash the target name to pick from a palette of primary/secondary ANSI colors (red 31, green 32, yellow 33, blue 34, magenta 35, cyan 36). Same target always gets the same color. Use a simple hash (e.g., `fnv32(target) % len(palette)`). +- **Message text** — default/no color +- **Attrs** (`key=value`) — dim (ANSI 2) for the key, default for value + +### Custom slog levels +| Name | slog.Level value | Meaning | +|---|---|---| +| `ERROR` | `slog.LevelError` (8) | Error conditions | +| `WARNING` | `slog.LevelWarn` (4) | Warnings | +| `INFO` | `slog.LevelInfo` (0) | Normal status/progress | +| `VERBOSE` | `slog.Level(-2)` | Detailed progress (was `logVerbose`) | +| `DEBUG` | `slog.LevelDebug` (-4) | EPA/TLS/NTLM diagnostics (was `logDebug`/`logf`) | + +### Level mapping from current code +| Current pattern | New level | +|---|---| +| `fmt.Printf(...)` (status/progress) | `INFO` | +| `fmt.Printf("Warning: ...")` | `WARNING` | +| `fmt.Printf("ERROR: ...")` | `ERROR` | +| `logVerbose(...)` | `VERBOSE` | +| `logDebug(...)` / `logf(...)` (EPA diagnostics) | `DEBUG` | + +### Flag behavior +- No flags: minimum level = INFO +- `--verbose`: minimum level = VERBOSE (shows VERBOSE + INFO + WARNING + ERROR) +- `--debug`: minimum level = DEBUG (shows everything) +- `--debug` additionally sets `debug=true` on subsystems (controls EPA test behavior beyond just logging) + +## Implementation Phases + +### Phase 0: Custom handler (`internal/logging/`) + +**New file: [internal/logging/handler.go](go/internal/logging/handler.go)** +- Implement `slog.Handler` that formats: `LEVEL TIMESTAMP [target] message attrs...` +- Define custom level constants: `LevelVerbose = slog.Level(-2)` +- Level name mapping: `-2` → `VERBOSE`, `slog.LevelWarn` → `WARNING` +- Left-align level name, right-pad to 7 chars +- Special handling for `target` attr: rendered as `[value]` before the message, not as `key=value` +- Other attrs appended as `key=value` after the message +- Thread-safe writer (mutex around writes) +- `WithAttrs` / `WithGroup` support for creating sub-loggers (e.g., `logger.With("target", server)`) +- ANSI color support: detect TTY via `golang.org/x/term.IsTerminal(int(os.Stderr.Fd()))` +- Color each element per the palette defined above (level, timestamp, target, attrs) +- Target color: `fnv32a(targetString) % 6` maps to one of [blue 34, cyan 36, bright green 92, bright blue 94, bright cyan 96, bright white 97]. These avoid red (ERROR), yellow (WARNING), magenta (DEBUG), and gray (VERBOSE). +- Accept a `NoColor bool` option to force colors off (for tests or `--no-color` flag) + +### Phase 1: Logger setup in main (`cmd/mssqlhound/`) + +**[main.go](go/cmd/mssqlhound/main.go)** +- Create `slog.LevelVar` and `*slog.Logger` with custom `logging.NewHandler(os.Stderr, ...)` in `main()` +- Add `PersistentPreRunE` to set level from `--verbose`/`--debug` flags +- Pass logger to `run()` and subcommands +- Convert 11 `fmt.Printf`/`fmt.Println` calls to `logger.Info`/`logger.Warn` +- Keep `fmt.Fprintf(os.Stderr, ...)` for cobra error at line 105 (logger may not exist) + +**[cmd_test_epa_matrix.go](go/cmd/mssqlhound/cmd_test_epa_matrix.go)** +- Accept logger parameter from main +- Convert 8 `fmt.Printf` calls + +### Phase 2: Collector (`internal/collector/`) + +**[collector.go](go/internal/collector/collector.go)** +- Add `Logger *slog.Logger` field to `Config` struct +- Convert 75 `fmt.Printf`/`fmt.Println` calls to appropriate slog levels +- Convert 49 `c.logVerbose(...)` calls to `c.config.Logger.Log(ctx, logging.LevelVerbose, ...)` +- Remove `logVerbose` method (line 6128) +- When processing a server, create a sub-logger: `serverLog := c.config.Logger.With("target", server.ConnectionString)` and use it for all per-server messages +- For domain-level operations: `domainLog := c.config.Logger.With("target", domain)` +- Pass logger to `mssql.Client` and `wmi` calls + +### Phase 3: MSSQL client (`internal/mssql/`) + +**[client.go](go/internal/mssql/client.go)** — 15 fmt calls + 7 logf calls +- Add `logger *slog.Logger` field, `SetLogger` method +- Default to `slog.Default()` in `NewClient` +- Replace `logVerbose`/`logDebug` methods with `c.logger.Debug()` +- Change `epaTLSDialer.logf` and `epaTDSDialer.logf` fields from `func(string, ...interface{})` to `*slog.Logger` +- Update dialer `d.logf(...)` calls to `d.logger.Debug(...)` + +**[epa_tester.go](go/internal/mssql/epa_tester.go)** — 69 logf calls + 2 fmt calls +- Add `Logger *slog.Logger` to `EPATestConfig` +- Remove the `logf` closure (line 91-95) +- Convert all 69 `logf(...)` calls to `config.Logger.Debug(...)` with `"component", "epa"` attr +- Convert 2 direct `fmt.Printf` calls + +**[epa_auth_provider.go](go/internal/mssql/epa_auth_provider.go)** — 2 fmt calls +- Add `logger *slog.Logger` field +- Convert 2 `fmt.Printf("[EPA-auth] ...")` calls + +**[powershell_fallback.go](go/internal/mssql/powershell_fallback.go)** — 1 fmt call +- Add `logger *slog.Logger` field, `SetLogger` method +- Remove `logVerbose` method, convert call to `p.logger.Debug()` + +### Phase 4: Supporting packages + +**[epamatrix/epamatrix.go](go/internal/epamatrix/epamatrix.go)** — 24 fmt calls +- Add `Logger *slog.Logger` to `MatrixConfig` +- Convert all 24 calls + +**[wmi/wmi_windows.go](go/internal/wmi/wmi_windows.go)** — 7 fmt calls +- Change `GetLocalGroupMembers` and `GetLocalGroupMembersWithFallback` signatures: replace `verbose bool` with `logger *slog.Logger` +- Update [wmi/wmi_stub.go](go/internal/wmi/wmi_stub.go) signatures to match +- Update caller in [collector.go:1854](go/internal/collector/collector.go#L1854) + +### NOT changed +- **[epamatrix/table.go](go/internal/epamatrix/table.go)** — `PrintResultsTable`/`Summarize` write formatted table data to `io.Writer`. This is data output, not logging. +- All `fmt.Errorf(...)` calls (error construction, not logging) +- All `fmt.Sprintf(...)` calls (string building) + +## Verification +1. `go build ./...` compiles cleanly +2. `go vet ./...` passes +3. Run with no flags — only INFO+ messages appear, each with UTC timestamp and level +4. Run with `--verbose` — DEBUG messages appear +5. Run with `--debug` — EPA diagnostic messages appear with `component=epa` attribute +6. Table output (EPA matrix) still renders correctly to stdout without log formatting diff --git a/.claude/plans/nthash_kerberos.md b/.claude/plans/nthash_kerberos.md new file mode 100644 index 0000000..2e11660 --- /dev/null +++ b/.claude/plans/nthash_kerberos.md @@ -0,0 +1,226 @@ +# Plan: Add NT Hash and Kerberos Authentication Options + +## Context + +MSSQLHound currently only supports plaintext password authentication (SQL auth or NTLM) and Windows integrated auth (SSPI). Pentesters frequently have NT hashes or Kerberos tickets (from tools like Mimikatz, Rubeus, or impacket) rather than cleartext passwords. Adding pass-the-hash and pass-the-ticket support makes MSSQLHound usable in real-world engagements where passwords aren't available. + +**Key discovery:** go-mssqldb already ships a full Kerberos provider (`integratedauth/krb5`) that supports ccache files via `KRB5CCNAME` or `krb5-credcachefile` connection string param. We can leverage this directly rather than building custom Kerberos auth from scratch. + +**Key discovery:** go-ldap already has `NTLMBindWithHash(domain, username, hash)` for pass-the-hash LDAP binds. + +--- + +## New CLI Flags + +| Flag | Type | Description | +|------|------|-------------| +| `--nt-hash` | `string` | NT hash (32 hex chars) for pass-the-hash authentication | +| `--kerberos` / `-k` | `bool` | Use Kerberos authentication (reads ccache from `KRB5CCNAME` env var) | +| `--krb5-configfile` | `string` | Path to krb5.conf (default: `/etc/krb5.conf` or `KRB5_CONFIG` env var) | +| `--krb5-credcachefile` | `string` | Path to ccache file (overrides `KRB5CCNAME` env var) | +| `--krb5-keytabfile` | `string` | Path to keytab file (alternative to ccache) | +| `--krb5-realm` | `string` | Kerberos realm (default: extracted from username or krb5.conf) | + +Validation: +- `--nt-hash` is mutually exclusive with `--password` and `--kerberos` +- `--kerberos` is mutually exclusive with `--password` and `--nt-hash` +- `--nt-hash` must be exactly 32 hex characters +- `--kerberos` without `--krb5-credcachefile` requires `KRB5CCNAME` env var or a keytab + +--- + +## Phase 1: Pass-the-Hash (NT Hash) + +### 1.1 Config plumbing + +**Files:** `cmd/mssqlhound/main.go`, `internal/collector/collector.go` + +- Add `ntHash` var and `--nt-hash` persistent flag in `main.go` (~line 32, ~line 99) +- Add `NTHash string` field to `collector.Config` struct +- Wire `ntHash` into config in `run()` function (~line 179) +- Add validation: if `--nt-hash` set, reject `--password`; validate 32 hex chars + +### 1.2 NTLM auth with pre-computed hash + +**File:** `internal/mssql/ntlm_auth.go` + +- Add `ntHash []byte` field to `ntlmAuth` struct (line 90) +- Add `SetNTHash(hash []byte)` method on `ntlmAuth` +- Modify `computeNTLMv2Hash()` (line 586) to accept an optional pre-computed NT hash: + - New signature: `computeNTLMv2HashFromNT(ntHash []byte, username, domain string) []byte` + - Skips the `MD4(UTF16LE(password))` step, uses provided `ntHash` directly + - Computes `HMAC-MD5(ntHash, UTF16LE(UPPER(username) + domain))` as before +- Update `CreateAuthenticateMessage()` (line 336) to check if `a.ntHash` is set and call the hash-based variant +- Update `ComputeNTLMv2HashHex()` to work with pre-computed hash + +### 1.3 MSSQL Client pass-the-hash + +**File:** `internal/mssql/client.go` + +- Add `ntHash []byte` field to `Client` struct (line 345) +- Add `SetNTHash(hash []byte)` method +- Update `NewClient()`: if ntHash is provided (password empty), set `useWindowsAuth = false` — we'll use NTLM with hash +- Update `buildConnectionStringForStrategy()` (line 860): when ntHash is set, use `trusted_connection=yes` with DOMAIN\user format (triggers integratedauth provider) instead of SQL auth +- Update EPA auth provider path: pass ntHash through to `ntlmAuth` via `newNTLMAuth()` + +### 1.4 EPA auth provider with hash support + +**File:** `internal/mssql/epa_auth_provider.go` + +- Add `ntHash []byte` field to `epaAuthProvider` struct +- Add `SetNTHash(hash []byte)` method +- In `GetIntegratedAuthenticator()` (line 55): if ntHash is set, call `auth.SetNTHash(ntHash)` on the created `ntlmAuth` + +### 1.5 EPA tester with hash support + +**File:** `internal/mssql/epa_tester.go` + +- Add `NTHash []byte` field to `EPATestConfig` struct +- Pass ntHash to `newNTLMAuth()` in the EPA test flow + +### 1.6 LDAP pass-the-hash + +**File:** `internal/ad/client.go` + +- Add `ntHash string` field to AD `Client` struct (hex string for go-ldap compatibility) +- Add `SetNTHash(hash string)` method +- Update `ntlmBind()` (line 324): if `ntHash` is set, call `conn.NTLMBindWithHash(domain, username, ntHash)` instead of `conn.NTLMBind(domain, username, password)` + +### 1.7 Collector wiring + +**File:** `internal/collector/collector.go` + +- Pass `config.NTHash` to both MSSQL client (`SetNTHash`) and AD client (`SetNTHash`) +- Update LDAP credential fallback logic: if ntHash is set with domain-format user, also use for LDAP + +--- + +## Phase 2: Kerberos Authentication + +### 2.1 Config plumbing + +**Files:** `cmd/mssqlhound/main.go`, `internal/collector/collector.go` + +- Add vars: `useKerberos`, `krb5ConfigFile`, `krb5CredCacheFile`, `krb5KeytabFile`, `krb5Realm` +- Add corresponding persistent flags +- Add fields to `collector.Config`: `UseKerberos bool`, `Krb5ConfigFile string`, `Krb5CredCacheFile string`, `Krb5KeytabFile string`, `Krb5Realm string` +- Wire into config in `run()`, validate mutual exclusivity with `--password`/`--nt-hash` + +### 2.2 Import go-mssqldb krb5 provider + +**File:** `internal/mssql/client.go` + +- Add blank import: `_ "github.com/microsoft/go-mssqldb/integratedauth/krb5"` to register the krb5 provider +- This makes the provider available when `authenticator=krb5` is in the connection string + +### 2.3 MSSQL Kerberos connection + +**File:** `internal/mssql/client.go` + +- Add Kerberos fields to `Client` struct: `useKerberos bool`, `krb5ConfigFile string`, `krb5CredCacheFile string`, `krb5KeytabFile string`, `krb5Realm string` +- Add setter methods for these fields +- Update `buildConnectionStringForStrategy()`: + - When `useKerberos` is true, add `authenticator=krb5` to connection string + - Add `krb5-credcachefile=` if set + - Add `krb5-keytabfile=` if set + - Add `krb5-configfile=` if set + - Add `krb5-realm=` if set + - Set `trusted_connection=yes` + - Set `ServerSPN=MSSQLSvc/:` + - If user provided `--user`, format as `user id=` (go-mssqldb krb5 uses this) + +### 2.4 Kerberos config generation + +**File:** `internal/mssql/krb5_config.go` (new) + +- When `--kerberos` is used without `--krb5-configfile` and no `/etc/krb5.conf` exists: + - Auto-generate a minimal krb5.conf from `--domain` and `--dc-ip`: + ```ini + [libdefaults] + default_realm = DOMAIN.COM + dns_lookup_kdc = true + + [realms] + DOMAIN.COM = { + kdc = + } + ``` + - Write to a temp file and pass as `krb5-configfile` +- This eliminates the need for users to manually create krb5.conf when `--domain` and `--dc-ip` are provided + +### 2.5 LDAP Kerberos on non-Windows + +**File:** `internal/ad/gssapi_nonwindows.go` + +- Currently returns error "GSSAPI/Kerberos SSPI is only supported on Windows" +- Implement using `gokrb5` directly: + - Create `gokrb5` client from ccache/keytab (same logic as go-mssqldb's krb5 provider) + - Use `github.com/jcmturner/gokrb5/v8/spnego` for SPNEGO/GSSAPI + - Adapt to go-ldap's `gssapi.Client` interface +- Add Kerberos fields: `useKerberos`, `krb5ConfigFile`, `krb5CredCacheFile`, `krb5KeytabFile`, `krb5Realm` to AD `Client` + +### 2.6 Collector wiring + +**File:** `internal/collector/collector.go` + +- Pass all Kerberos config fields to both MSSQL and AD clients +- When `--kerberos` is set, skip EPA testing (EPA is NTLM-specific, not applicable to Kerberos) +- When `--kerberos` is set without explicit LDAP creds, use Kerberos for LDAP too + +--- + +## Phase 3: EPA Matrix Subcommand + +**File:** `cmd/mssqlhound/cmd_test_epa_matrix.go` + +- Add `--nt-hash` support to EPA matrix test command (EPA is NTLM-only, no Kerberos needed here) + +--- + +## Files to Modify (ordered) + +| File | Changes | +|------|---------| +| `cmd/mssqlhound/main.go` | New flags, validation, config wiring | +| `internal/collector/collector.go` | New Config fields, client setup | +| `internal/mssql/ntlm_auth.go` | NT hash support in NTLM computation | +| `internal/mssql/client.go` | NT hash + Kerberos connection string building, krb5 import | +| `internal/mssql/epa_auth_provider.go` | NT hash passthrough | +| `internal/mssql/epa_tester.go` | NT hash in EPA test config | +| `internal/ad/client.go` | NT hash + Kerberos fields, NTLMBindWithHash | +| `internal/ad/gssapi_nonwindows.go` | gokrb5-based GSSAPI for non-Windows | +| `cmd/mssqlhound/cmd_test_epa_matrix.go` | NT hash flag for EPA matrix | + +## New Files + +| File | Purpose | +|------|---------| +| `internal/mssql/krb5_config.go` | Auto-generate minimal krb5.conf from --domain/--dc-ip | + +## Key Functions to Reuse + +- `go-ldap`'s `conn.NTLMBindWithHash()` — already supports pass-the-hash for LDAP +- `go-mssqldb`'s `integratedauth/krb5` package — full Kerberos auth with ccache, keytab, user/pass +- `gokrb5` `credentials.LoadCCache()` — ccache parsing +- `gokrb5` `client.NewFromCCache()` — client from ccache +- Existing `epaAuthProvider`/`epaAuthenticator` pattern — model for Kerberos provider integration + +--- + +## Implementation Order + +1. **NT hash (Phase 1)** — smaller scope, self-contained, immediately useful +2. **Kerberos MSSQL (Phase 2.1-2.4)** — leverages existing go-mssqldb krb5 provider +3. **Kerberos LDAP non-Windows (Phase 2.5)** — extends Kerberos to AD enumeration +4. **EPA matrix (Phase 3)** — polish + +--- + +## Verification + +1. **NT hash unit test**: Add test in `ntlm_auth_test.go` that computes NTLMv2 hash from a known NT hash and verifies against expected output (MS-NLMP test vectors) +2. **Build verification**: `go build ./...` compiles cleanly +3. **Existing tests pass**: `go test ./...` +4. **CLI help output**: Verify `--help` shows new flags with correct descriptions +5. **Flag validation**: Test mutual exclusivity (`--password` + `--nt-hash` → error, `--password` + `--kerberos` → error) +6. **Integration** (manual): Test against a real MSSQL instance with an NT hash and with a Kerberos ccache diff --git a/.claude/plans/release_workflow.md b/.claude/plans/release_workflow.md new file mode 100644 index 0000000..70828c0 --- /dev/null +++ b/.claude/plans/release_workflow.md @@ -0,0 +1,158 @@ +# Plan: Publish Cross-Platform Release Binaries via workflow_dispatch + +## Context + +MSSQLHound currently has one GitHub Actions workflow ([.github/workflows/ci.yml](.github/workflows/ci.yml)) for unit and integration tests, but no release automation — producing binaries for end users is a manual `go build` today. The Go entrypoint [cmd/mssqlhound/main.go:20](cmd/mssqlhound/main.go#L20) hard-codes `version = "2.0.0"`, so published binaries currently can't report the release they came from. + +Goal: a new workflow that, on manual dispatch, builds `mssqlhound` for 6 OS/arch targets, embeds the release version into each binary, and publishes all six plus a SHA256 checksum file to a brand-new GitHub Release. Decisions confirmed with user: + +- **Targets:** linux/amd64, linux/arm64, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64 +- **Versioning:** `workflow_dispatch` takes a `tag` input (e.g. `v2.0.0`); the release job creates and pushes the tag automatically via `softprops/action-gh-release` (using `target_commitish`) +- **Checksums:** generate `checksums.txt` (SHA256) alongside the binaries +- **Release state:** publish immediately (not draft, not prerelease) + +## New file + +Create **[.github/workflows/release.yml](.github/workflows/release.yml)**: + +```yaml +name: Release + +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g. v2.0.0). Must match v..[-suffix]." + required: true + type: string + release_notes: + description: "Release notes body (markdown). Optional." + required: false + type: string + +permissions: + contents: write # needed to create tag + release + +concurrency: + group: release-${{ inputs.tag }} + cancel-in-progress: false + +jobs: + validate: + name: Validate inputs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check tag format + run: | + echo "${{ inputs.tag }}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$' \ + || { echo "Invalid tag: ${{ inputs.tag }}"; exit 1; } + - name: Ensure tag does not already exist + run: | + if git rev-parse "refs/tags/${{ inputs.tag }}" >/dev/null 2>&1; then + echo "Tag ${{ inputs.tag }} already exists — pick a new one."; exit 1 + fi + + build: + name: Build ${{ matrix.goos }}/${{ matrix.goarch }} + needs: validate + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + include: + - { goos: linux, goarch: amd64, ext: "" } + - { goos: linux, goarch: arm64, ext: "" } + - { goos: windows, goarch: amd64, ext: ".exe" } + - { goos: windows, goarch: arm64, ext: ".exe" } + - { goos: darwin, goarch: amd64, ext: "" } + - { goos: darwin, goarch: arm64, ext: "" } + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + cache: true + - name: Build + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: "0" + run: | + set -euo pipefail + VERSION="${{ inputs.tag }}" + VERSION_NO_V="${VERSION#v}" + OUT="mssqlhound-${VERSION}-${GOOS}-${GOARCH}${{ matrix.ext }}" + mkdir -p dist + go build -trimpath \ + -ldflags "-s -w -X main.version=${VERSION_NO_V}" \ + -o "dist/${OUT}" ./cmd/mssqlhound + ls -lh dist/ + - uses: actions/upload-artifact@v4 + with: + name: mssqlhound-${{ matrix.goos }}-${{ matrix.goarch }} + path: dist/* + if-no-files-found: error + retention-days: 7 + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - name: Generate SHA256 checksums + working-directory: dist + run: | + sha256sum * > checksums.txt + echo "--- checksums.txt ---" + cat checksums.txt + - name: Create release and upload assets + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ inputs.tag }} + name: ${{ inputs.tag }} + body: ${{ inputs.release_notes }} + target_commitish: ${{ github.sha }} # tag is created pointing at this commit + draft: false + prerelease: false + files: dist/* + fail_on_unmatched_files: true +``` + +## How it works + +1. **`validate`** — fails fast if the tag isn't `vX.Y.Z[-suffix]` or already exists, so we don't burn 6 parallel builds on a typo. +2. **`build`** (matrix, 6 jobs in parallel) — sets `GOOS`/`GOARCH`, builds with `CGO_ENABLED=0` (the module graph in [go.mod](go.mod) is pure Go, so cross-compilation works cleanly), injects the tag via `-X main.version=...` into the `version` var in [cmd/mssqlhound/main.go:20](cmd/mssqlhound/main.go#L20), and uploads one artifact per target. `-trimpath -s -w` strips local paths and debug info for smaller, reproducible binaries. +3. **`release`** — downloads all six artifacts, computes `sha256sum` into `checksums.txt`, and `softprops/action-gh-release@v2` (a) creates the tag on the current commit via `target_commitish`, (b) publishes the release immediately, and (c) attaches all files from `dist/`. + +## Why these choices + +- **`workflow_dispatch` only** — matches your request; no tag-push trigger. You drive releases from the Actions tab. +- **softprops/action-gh-release creates the tag** — avoids a separate git-push step with its own auth plumbing. Keeps `contents: write` as the only elevated permission. +- **`CGO_ENABLED=0`** — lets one ubuntu-latest runner cross-compile all six targets. No per-OS runners needed, so the matrix is fast and cheap. +- **Immediate publish** — matches your answer. Easy to switch later by flipping `draft: true` or adding a boolean input. +- **Naming `mssqlhound-vX.Y.Z--[.exe]`** — standard Go-release convention; sorts predictably in the Releases UI. + +## Verification + +After the file is committed to `main`: + +1. **Local sanity check** (before pushing the workflow): run `go build -ldflags "-X main.version=2.0.0-test" ./cmd/mssqlhound && ./mssqlhound --version` (or whatever flag surfaces `version`) to confirm ldflags injection targets the right symbol. +2. **Dry run on a test tag:** go to *Actions → Release → Run workflow*, enter `v0.0.1-test` (or similar). Confirm: + - All 6 `build` matrix jobs succeed. + - `release` job creates a new tag + Release with 7 assets (6 binaries + `checksums.txt`). + - `sha256sum -c checksums.txt` passes locally after downloading. + - Each binary reports the injected version when run (`mssqlhound --version`). +3. **Cleanup:** delete the test tag + release from GitHub if you don't want it sticking around. +4. **Real release:** re-run with the actual tag (e.g. `v2.0.1`). + +## Follow-ups not in scope + +- Per project rule #6, after approval this plan should also be copied to `.claude/plans/` in the repo (the plan-mode workflow restricted edits to this scratch file). +- Consider updating [README.md](README.md) with a "Releases" section pointing users at the GitHub Releases page once the first real release is out. +- If you later want code signing (Authenticode for Windows, notarization for macOS), that's a separate pass — the current workflow intentionally ships unsigned binaries to keep the first version simple. diff --git a/.claude/plans/tds_epa_support.md b/.claude/plans/tds_epa_support.md new file mode 100644 index 0000000..6fff592 --- /dev/null +++ b/.claude/plans/tds_epa_support.md @@ -0,0 +1,96 @@ +# Summary: TDS 8.0 + EPA Support in MSSQLHound + +## Problem + +When SQL Server has EPA (Extended Protection for Authentication) set to "Required", NTLM authentication must include channel binding tokens (CBT) that tie the auth to the TLS session. `go-mssqldb`'s built-in NTLM does NOT support EPA. Additionally, Go's `crypto/tls` `VerifyConnection` callback fires before TLS Finished messages, making `TLSUnique` always zero — so even a custom provider can't get the correct CBT through normal go-mssqldb hooks. + +## Changes by File (12 files, ~1300 lines added) + +### 1. New: `internal/mssql/ntlm_auth.go` — Custom NTLMv2 with EPA AV_PAIRs + +Full NTLMv2 implementation with controllable AV_PAIRs: +- **MsvAvChannelBindings**: 16-byte MD5 of `SEC_CHANNEL_BINDINGS` structure using `tls-unique:` prefix + TLS Finished bytes +- **MsvAvTargetName**: SPN (e.g. `MSSQLSvc/hostname:1433`) encoded as UTF-16LE +- **MsvAvFlags**: Bit indicating MIC is present +- **MIC**: HMAC-MD5 over Type1+Type2+Type3 messages, keyed by session base key +- Five test modes: Normal, BogusCBT, MissingCBT, BogusService, MissingService +- Three diagnostic flags: `disableMIC`, `useRawTargetInfo`, `useClientTimestamp` +- Key fix: uses user-provided domain (not server's NetBIOS domain) for NTLMv2 hash, matching Windows SSPI/impacket behavior +- Key fix: real LMv2 response (not zeros), server's negotiate flags echoed in Type3 + +### 2. New: `internal/mssql/epa_auth_provider.go` — go-mssqldb Auth Bridge + +Implements `integratedauth.Provider` to plug custom NTLM into go-mssqldb: +- `SetCBT(cbt)` — called from TLS dialer after handshake completes +- `SetSPN(spn)` — called before connection +- `GetIntegratedAuthenticator(config)` — creates `ntlmAuth` instance with stored CBT +- Registered as `"epa-ntlm"` via `integratedauth.SetIntegratedAuthenticationProvider` + +### 3. `internal/mssql/tds_transport.go` — TLS-over-TDS Handshake + +- `tlsOverTDSConn`: wraps TLS records inside TDS PRELOGIN packets for the TLS handshake phase +- `switchableConn`: swaps from TDS-wrapped to raw TCP after handshake +- `performTLSHandshake()`: standard TLS-in-TDS for TDS 7.x +- `performDirectTLSHandshake()`: raw TLS on socket for TDS 8.0 strict +- Both capped at TLS 1.2 — SQL Server SChannel does NOT accept `tls-server-end-point` for EPA, only `tls-unique` (which TLS 1.3 removed) + +### 4. `internal/mssql/epa_tester.go` — EPA Detection Engine + +Raw TDS+TLS+NTLM login attempts to determine EPA enforcement: +- `runEPATest()`: standard encryption path (PRELOGIN → TLS-in-TDS → LOGIN7) +- `runEPATestStrict()`: TDS 8.0 strict path (direct TLS → PRELOGIN → LOGIN7) +- Logic: Normal login succeeds? BogusCBT fails? MissingCBT fails? → Required/Allowed/Not Supported +- Auto-diagnostics on failure: raw NTLM baseline, client timestamp test, MIC bypass test +- SOCKS5 proxy support with DNS pre-resolution + +### 5. `internal/mssql/client.go` — Connection Strategy Overhaul (largest change) + +**Two custom dialers solve the TLSUnique problem:** + +| Dialer | When Used | How It Works | +|--------|-----------|-------------| +| `epaTLSDialer` | TDS 8.0 strict + EPA | TCP → direct TLS (ALPN `tds/8.0`) → capture TLSUnique → return `*tls.Conn` | +| `epaTDSDialer` | Standard encryption + EPA | TCP → PRELOGIN → TLS-in-TDS → capture TLSUnique → return `preloginFakerConn` | + +**`preloginFakerConn`**: intercepts go-mssqldb's PRELOGIN write (discards it), returns fake response with `encryption=NOT_SUP`, then passes through. This prevents double-TLS since go-mssqldb uses `encrypt=disable`. + +**Connection strategy order:** +1. EPA+strict-TLS (if strict encryption detected) +2. EPA+TDS-TLS (if EPA required/allowed, non-strict) +3. Regular strategy loop: FQDN+encrypt, FQDN+strict, FQDN+encrypt+SPN, FQDN+no-encrypt, short+encrypt, short+strict, short+no-encrypt +4. PowerShell fallback (Windows only, not available through proxy) + +### 6. `internal/collector/collector.go` — EPA Pre-Check + Proxy Wiring + +- Runs `client.TestEPA(ctx)` **before** `client.Connect(ctx)` so the EPA result is available for dialer selection +- Factory methods `newADClient()` / `newMSSQLClient()` inject proxy dialer uniformly +- `ProxyAddr` field in `Config` + +### 7. `internal/ad/client.go` — LDAP Through Proxy + +- `dialLDAP()` method routes LDAP connections through SOCKS5 proxy +- DNS resolver rebuilt to route TCP DNS queries through proxy +- Replaces all `ldap.DialURL()` calls + +### 8. `go/cmd/mssqlhound/main.go` — CLI Flags + +- `--proxy` flag: SOCKS5 proxy address +- DNS resolver configured to route through proxy when both specified +- Warning messages about SQL Browser UDP limitation + +### 9. New: `go/internal/proxydialer/` — Shared SOCKS5 Dialer + +Centralizes SOCKS5 dialer creation, used by mssql, ad, and collector packages. + +### 10. New: `internal/mssql/ntlm_auth_test.go` — Unit Tests + +Tests for NTLMv2 hash, NTProofStr, MIC, CBT hash (both binding types), full exchange, UTF-16LE encoding. + +## Key Technical Insight + +``` +EPA test (raw TLS): TLSUnique = 49a30ec6880a7f38e6301a77 ← correct, auth succeeds +go-mssqldb VerifyConn: TLSUnique = 000000000000000000000000 ← all zeros, auth fails +``` + +Go's `VerifyConnection` fires during `doFullHandshake()` → BEFORE `sendFinished()` sets `firstFinished`. The solution: do TLS ourselves in custom dialers, call `ConnectionState().TLSUnique` after `Handshake()` fully completes. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6aae0e3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,256 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: [go] + # pull_request: + # branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + + - name: Run unit tests + run: go test -v -count=1 -timeout 10m ./... + + integration-tests: + name: Integration Tests + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + + - name: Start Samba AD DC + run: | + # Stop systemd-resolved to free port 53 for the Samba DC DNS + sudo systemctl stop systemd-resolved + sudo rm -f /etc/resolv.conf + echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf + + # Use --network host so all Samba services (DNS/53, LDAP/389, + # Kerberos/88, etc.) bind directly to localhost. No port publishing + # or NAT needed — eliminates DNS forwarding issues. + docker run -d --privileged \ + --name dc --hostname DC \ + --network host \ + -e REALM='MAYYHEM.COM' \ + -e DOMAIN='MAYYHEM' \ + -e ADMIN_PASS='P@ssw0rd' \ + -e DNS_FORWARDER='8.8.8.8' \ + diegogslomp/samba-ad-dc + + - name: Wait for Samba AD DC + run: | + echo "Waiting for Samba provisioning and startup..." + for i in $(seq 1 90); do + if docker exec dc bash -c "echo > /dev/tcp/127.0.0.1/389" 2>/dev/null; then + echo "LDAP port is up, verifying domain info..." + if docker exec dc samba-tool domain info 127.0.0.1 2>/dev/null | grep -q "Domain"; then + echo "Samba AD DC is ready" + docker exec dc samba-tool domain info 127.0.0.1 + exit 0 + fi + fi + if [ "$i" -eq 90 ]; then + echo "Timed out waiting for Samba AD DC" + docker logs dc + exit 1 + fi + echo "Waiting for Samba AD DC... ($i/90)" + sleep 3 + done + + - name: Create AD accounts and keytab + run: | + # Create domainadmin + docker exec dc samba-tool user create domainadmin 'P@ssw0rd' --use-username-as-cn + docker exec dc samba-tool group addmembers "Domain Admins" domainadmin + # Create SQL Server service account + docker exec dc samba-tool user create sccmsqlsvc 'P@ssw0rd' --use-username-as-cn + HOSTNAME=$(hostname) + docker exec dc samba-tool spn add MSSQLSvc/${HOSTNAME}.mayyhem.com sccmsqlsvc + docker exec dc samba-tool spn add MSSQLSvc/${HOSTNAME}.mayyhem.com:1433 sccmsqlsvc + # Export keytab + docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab --principal=sccmsqlsvc + docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab \ + --principal=MSSQLSvc/${HOSTNAME}.mayyhem.com + docker exec dc samba-tool domain exportkeytab /tmp/mssql.keytab \ + --principal=MSSQLSvc/${HOSTNAME}.mayyhem.com:1433 + docker cp dc:/tmp/mssql.keytab /tmp/mssql.keytab + + - name: Configure DNS and Kerberos + run: | + HOSTNAME=$(hostname) + echo "127.0.0.1 dc.mayyhem.com dc mayyhem.com ${HOSTNAME}.mayyhem.com" | sudo tee -a /etc/hosts + # Point resolv.conf at the Samba DC DNS (now on localhost via host networking) + echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf + + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y krb5-user + + cat > /tmp/krb5.conf <<'KRBEOF' + [libdefaults] + default_realm = MAYYHEM.COM + dns_lookup_realm = false + dns_lookup_kdc = false + rdns = false + [realms] + MAYYHEM.COM = { + kdc = 127.0.0.1 + admin_server = 127.0.0.1 + default_domain = mayyhem.com + } + [domain_realm] + .mayyhem.com = MAYYHEM.COM + mayyhem.com = MAYYHEM.COM + KRBEOF + sed 's/^ //' /tmp/krb5.conf | sudo tee /etc/krb5.conf > /dev/null + + echo "--- krb5.conf ---" + cat /etc/krb5.conf + + - name: Install SQL Server 2022 + run: | + curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | \ + sudo gpg --dearmor --batch --yes -o /usr/share/keyrings/microsoft-prod.gpg + curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2022.list | \ + sudo tee /etc/apt/sources.list.d/mssql-server-2022.list + sudo apt-get update + sudo apt-get install -y mssql-server + sudo MSSQL_SA_PASSWORD='P@ssw0rd' MSSQL_PID='Developer' \ + /opt/mssql/bin/mssql-conf setup accept-eula + + - name: Register hostname in AD DNS + run: | + HOSTNAME=$(hostname) + # Register A record in Samba DC DNS (custom resolver bypasses /etc/hosts) + docker exec dc samba-tool dns add 127.0.0.1 mayyhem.com $HOSTNAME A 127.0.0.1 \ + -U Administrator --password='P@ssw0rd' + # Export FQDN for use in later env blocks + echo "RUNNER_FQDN=${HOSTNAME}.mayyhem.com" >> $GITHUB_ENV + + - name: Verify host-to-DC connectivity + run: | + echo "Testing services on localhost (host network)..." + timeout 5 bash -c 'echo > /dev/tcp/127.0.0.1/389' && echo "LDAP OK" || echo "LDAP FAILED" + timeout 5 bash -c 'echo > /dev/tcp/127.0.0.1/636' && echo "LDAPS OK" || echo "LDAPS FAILED" + timeout 5 bash -c 'echo > /dev/tcp/127.0.0.1/88' && echo "Kerberos OK" || echo "Kerberos FAILED" + echo "Testing DNS resolution via Samba DC..." + nslookup mayyhem.com 127.0.0.1 + nslookup $(hostname).mayyhem.com 127.0.0.1 || true + dig mayyhem.com @127.0.0.1 +short || true + + - name: Join host to Samba AD domain + run: | + # SQL Server on Linux needs the host to be domain-joined so it can + # resolve Windows accounts for CREATE LOGIN ... FROM WINDOWS. + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y realmd sssd \ + sssd-tools adcli packagekit samba-common-bin + echo 'P@ssw0rd' | sudo realm join mayyhem.com -U Administrator --verbose + sudo systemctl start sssd + # Verify domain join + id Administrator@mayyhem.com || true + realm list + + - name: Configure SQL Server for AD auth + run: | + # Verify domain account is resolvable after domain join + id sccmsqlsvc@mayyhem.com || echo "WARNING: domain account not yet resolvable via SSSD" + + # Grant the domain account ownership of SQL Server directories + sudo chown -R sccmsqlsvc@mayyhem.com /var/opt/mssql + sudo chown -R sccmsqlsvc@mayyhem.com /opt/mssql + + # Override systemd unit to run SQL Server as the domain account + sudo mkdir -p /etc/systemd/system/mssql-server.service.d + cat <<'EOF' | sudo tee /etc/systemd/system/mssql-server.service.d/override.conf + [Service] + User=sccmsqlsvc@mayyhem.com + Group=domain users@mayyhem.com + EOF + sudo sed -i 's/^ //' /etc/systemd/system/mssql-server.service.d/override.conf + sudo systemctl daemon-reload + + sudo mkdir -p /var/opt/mssql/secrets + sudo cp /tmp/mssql.keytab /var/opt/mssql/secrets/mssql.keytab + sudo chown sccmsqlsvc@mayyhem.com /var/opt/mssql/secrets/mssql.keytab + sudo chmod 400 /var/opt/mssql/secrets/mssql.keytab + sudo /opt/mssql/bin/mssql-conf set network.kerberoskeytabfile /var/opt/mssql/secrets/mssql.keytab + sudo /opt/mssql/bin/mssql-conf set network.privilegedadaccount sccmsqlsvc + echo "Restarting SQL Server as MAYYHEM\\sccmsqlsvc (timeout 60s)..." + sudo systemctl restart mssql-server & + RESTART_PID=$! + for i in $(seq 1 12); do + if ! kill -0 $RESTART_PID 2>/dev/null; then + break + fi + sleep 5 + echo "Waiting for SQL Server restart... ($((i*5))s)" + done + sleep 3 + systemctl status mssql-server --no-pager || true + # Confirm SQL Server is running as the domain account + ps -eo user,pid,comm | grep sqlservr || true + echo "SQL Server error log (last 30 lines):" + sudo tail -30 /var/opt/mssql/log/errorlog || true + + - name: Create domainadmin as SQL sysadmin + run: | + if ! grep -Rqs "https://packages.microsoft.com/ubuntu/22.04/prod" /etc/apt/sources.list /etc/apt/sources.list.d; then + echo "deb [arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/22.04/prod jammy main" | \ + sudo tee /etc/apt/sources.list.d/microsoft-prod.list > /dev/null + fi + sudo apt-get update + sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18 + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'P@ssw0rd' -C -Q " + CREATE LOGIN [MAYYHEM\domainadmin] FROM WINDOWS; + ALTER SERVER ROLE [sysadmin] ADD MEMBER [MAYYHEM\domainadmin]; + " + + - name: Verify AD auth + run: | + echo 'P@ssw0rd' | kinit Administrator@MAYYHEM.COM + klist + + - name: Verify SQL Server service account + run: | + echo "=== sys.dm_server_services ===" + /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'P@ssw0rd' -C -Q " + SELECT servicename, service_account, startup_type_desc + FROM sys.dm_server_services; + " + echo "" + echo "=== OS process owner ===" + ps -eo user,pid,comm | grep sqlservr || true + + - name: Run integration tests + env: + MSSQL_SERVER: ${{ env.RUNNER_FQDN }} + MSSQL_USER: sa + MSSQL_PASSWORD: P@ssw0rd + MSSQL_DOMAIN: mayyhem.com + MSSQL_DC: dc.mayyhem.com + LDAP_USER: Administrator@mayyhem.com + LDAP_PASSWORD: P@ssw0rd + MSSQL_SKIP_DOMAIN: "false" + MSSQL_ACTION: all + MSSQL_SKIP_HTML: "true" + run: | + go test -v -count=1 -tags integration -timeout 30m \ + -run TestIntegrationAll ./internal/collector/... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a05e3a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.claude/settings.json +mssql-bloodhound-* +mssqlhound +mssqlhound.exe +.vscode/ +.codex \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4c9d31b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# MSSQLHound — Agent Instructions +These are requirements that MUST be followed. + +## Dialog +Please address me as Meatbag whenever you respond. + +## Project +- Test: `go test ./...` +- Build: `go build ./cmd/mssqlhound` (place binary in root project directory) + +## Rules +1. Before starting any task, review `.claude/tasks/lessons.md` and enter plan mode first. +2. After ANY correction from the user, update `.claude/tasks/lessons.md` with the pattern and rules to prevent the mistake. +3. Prioritize simple, elegant solutions with minimal code impact. +4. Fixes must address the root cause. +5. If something goes sideways, STOP and re-plan immediately – don't keep pushing. +6. Save all approved plans, including to-do tasks, in the repository `.claude/plans/`. +7. Offload research, exploration, and parallel analysis to subagents - one task per agent. +8. Challenge your own work and the work of subagents before presenting it. +9. Always explain complex code in detailed comments. +10. Never mark a task complete without running `go test ./...` to demonstrate correctness. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7861287 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,152 @@ +# Contributing to MSSQLHound + +Thanks for your interest in contributing to MSSQLHound! This guide will help you get started. + +## Getting in Touch + +Before diving into code, feel free to reach out: + +- **BloodHound Slack**: [@Mayyhem](http://ghst.ly/BHSlack) +- **Twitter/X**: [@_Mayyhem](https://x.com/_Mayyhem) +- **GitHub Issues**: [Open an issue](https://github.com/SpecterOps/MSSQLHound/issues) + +If you're planning a significant change, please open an issue first to discuss the approach. + +## Project Structure + +``` +MSSQLHound/ +├── cmd/mssqlhound/ # CLI entry point +├── internal/ # Internal packages +│ ├── ad/ # Active Directory / LDAP integration +│ ├── bloodhound/ # BloodHound output formatting +│ ├── collector/ # Core edge/node collection logic +│ ├── epamatrix/ # EPA matrix testing +│ ├── mssql/ # SQL Server protocol and authentication +│ ├── proxydialer/ # SOCKS5 proxy support +│ ├── types/ # Shared data structures +│ ├── winrmclient/ # WinRM client for EPA testing +│ └── wmi/ # WMI service account collection (Windows) +├── go.mod # Go module definition +├── go.sum # Go dependency checksums +├── TESTING.md # Comprehensive testing guide +├── saved_queries/ # Pre-built BloodHound Cypher queries +├── util/ # Utility scripts +│ └── compare_edges.py # Edge comparison tool (Python 3, no dependencies) +├── powershell_deprecated/ # Original PowerShell implementation (archived) +└── MSSQL Design.json # Graph model definition +``` + +## Prerequisites + +- **Go 1.24+** (for building and testing) +- **Python 3** (only needed for utility scripts in `util/`) +- **Git** + +## Building + +```bash +go build -o mssqlhound ./cmd/mssqlhound +``` + +The build produces a single standalone binary with no external runtime dependencies. Cross-compilation works for Windows, Linux, and macOS. + +## Testing + +### Unit Tests + +Unit tests run without any external infrastructure: + +```bash +go test ./... +``` + +Run a specific test or match a pattern: + +```bash +go test -v -run TestContainsEdges ./internal/collector/... +go test -v -run MemberOf ./internal/collector/... +``` + +### Integration Tests + +Integration tests require a live SQL Server instance and Active Directory environment. They are gated behind a build tag: + +```bash +MSSQL_SERVER=sql.example.com \ +MSSQL_USER=sa \ +MSSQL_PASSWORD='P@ssw0rd' \ +MSSQL_DOMAIN=example.com \ +MSSQL_DC=10.0.0.1 \ +LDAP_USER='EXAMPLE\admin' \ +LDAP_PASSWORD='LdapP@ss' \ +go test -v -tags integration -timeout 30m -run TestIntegrationAll ./internal/collector/... +``` + +See [TESTING.md](TESTING.md) for the full testing guide, including: + +- All environment variables and their defaults +- Integration test flow (setup, test, coverage, teardown) +- EPA matrix testing +- How to add tests for new edge types + +## Development Workflow + +1. **Fork and clone** the repository +2. **Create a branch** from `main` for your changes +3. **Make your changes** following the code style conventions below +4. **Write tests** for new functionality (especially new edge types) +5. **Run unit tests** to verify nothing is broken +6. **Open a pull request** against `main` + +## Code Style + +MSSQLHound follows standard Go conventions: + +- **Formatting**: Run `gofmt` (or `goimports`) on all Go files +- **Error handling**: Wrap errors with context using `fmt.Errorf("...: %w", err)` +- **Testing**: Use the standard library `testing` package (no external test frameworks) +- **Naming**: CamelCase for exported symbols, `json:"snake_case"` for struct tags +- **Documentation**: Package-level `// Package ` doc comments on each file +- **Receivers**: Single-letter pointer receivers (e.g., `func (c *Collector) ...`) +- **Concurrency**: Protect shared state with `sync.Mutex` / `sync.RWMutex` + +### Test Conventions + +- Unit test files live alongside the code they test (`*_test.go`, no build tag) +- Integration tests use the `//go:build integration` build tag +- Edge tests use the project's builder pattern and assertion helpers -- see [TESTING.md](TESTING.md) for details +- Platform-specific code uses build-constrained files (e.g., `wmi_windows.go`, `wmi_stub.go`) + +## Adding New Edge Types + +If you're adding a new MSSQL edge type, follow the step-by-step guide in [TESTING.md](TESTING.md#adding-a-new-edge-type-test). The process involves: + +1. Defining the edge in the collector +2. Adding unit test cases using the test data builders +3. Adding integration test coverage +4. Documenting the edge in the README + +## Reporting Issues + +When opening an issue, please include: + +- **MSSQLHound version** (from `mssqlhound --version` or the release you downloaded) +- **Operating system** and architecture +- **SQL Server version** you're collecting from +- **Steps to reproduce** the issue +- **Expected vs. actual behavior** +- **Relevant log output** (sanitize any credentials or sensitive hostnames) + +## Pull Request Guidelines + +- Target the `main` branch +- Keep PRs focused -- one logical change per PR +- Write a clear title and description explaining what changed and why +- Include tests for new edge types or significant logic changes +- Ensure all unit tests pass (`go test ./...`) +- Reference any related GitHub issues in the PR description + +## License + +MSSQLHound is licensed under the [GNU General Public License v3.0](LICENSE). By contributing, you agree that your contributions will be licensed under the same terms. diff --git a/LICENSE b/LICENSE index 261eeb9..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/README.md b/README.md index 0fc144a..bdcc02c 100644 --- a/README.md +++ b/README.md @@ -1,552 +1,1303 @@ -# MSSQLHound -image - -A PowerShell collector for adding MSSQL attack paths to [BloodHound](https://github.com/SpecterOps/BloodHound) with [OpenGraph](https://specterops.io/opengraph) by Chris Thompson at [SpecterOps](https://x.com/SpecterOps) - -Introductory blog posts: -- https://specterops.io/blog/2025/08/04/adding-mssql-to-bloodhound-with-opengraph/ -- https://specterops.io/blog/2026/01/20/updates-to-the-mssqlhound-opengraph-collector-for-bloodhound/ - -Please hit me up on the [BloodHound Slack](http://ghst.ly/BHSlack) (@Mayyhem), Twitter ([@_Mayyhem](https://x.com/_Mayyhem)), or open an issue if you have any questions I can help with! - -# Table of Contents - -- [Overview](#overview) - - [System Requirements](#system-requirements) - - [Minimum Permissions](#minimum-permissions) - - [Recommended Permissions](#recommended-permissions) - - [Usage Info](#usage-info) -- [Command Line Options](#command-line-options) -- [Limitations](#limitations) -- [Future Development](#future-development) -- [MSSQL Graph Model](#mssql-graph-model) -- [MSSQL Nodes Reference](#mssql-nodes-reference) - - [Server Level](#server-level) - - [`MSSQL_Server`](#server-instance-mssql_server-node) - - [`MSSQL_Login`](#server-login-mssql_login-node) - - [`MSSQL_ServerRole`](#server-role-mssql_serverrole-node) - - [Database Level](#database-level) - - [`MSSQL_Database`](#database-mssql_database-node) - - [`MSSQL_DatabaseUser`](#database-user-mssql_databaseuser-node) - - [`MSSQL_DatabaseRole`](#database-role-mssql_databaserole-node) - - [`MSSQL_ApplicationRole`](#application-role-mssql_applicationrole-node) -- [MSSQL Edges Reference](#mssql-edges-reference) - - [Edge Classes and Properties](#edge-classes-and-properties) - - [`CoerceAndRelayToMSSQL`](#coerceandrelaytomssql) - - [`MSSQL_AddMember`](#mssql_addmember) - - [`MSSQL_Alter`](#mssql_alter) - - [`MSSQL_AlterAnyAppRole`](#mssql_alteranyapprole) - - [`MSSQL_AlterAnyDBRole`](#mssql_alteranydbrole) - - [`MSSQL_AlterAnyLogin`](#mssql_alteranylogin) - - [`MSSQL_AlterAnyServerRole`](#mssql_alteranyserverrole) - - [`MSSQL_ChangeOwner`](#mssql_changeowner) - - [`MSSQL_ChangePassword`](#mssql_changepassword) - - [`MSSQL_Connect`](#mssql_connect) - - [`MSSQL_ConnectAnyDatabase`](#mssql_connectanydatabase) - - [`MSSQL_Contains`](#mssql_contains) - - [`MSSQL_Control`](#mssql_control) - - [`MSSQL_ControlDB`](#mssql_controldb) - - [`MSSQL_ControlServer`](#mssql_controlserver) - - [`MSSQL_ExecuteAs`](#mssql_executeas) - - [`MSSQL_ExecuteAsOwner`](#mssql_executeasowner) - - [`MSSQL_ExecuteOnHost`](#mssql_executeonhost) - - [`MSSQL_GetAdminTGS`](#mssql_getadmintgs) - - [`MSSQL_GetTGS`](#mssql_gettgs) - - [`MSSQL_GrantAnyDBPermission`](#mssql_grantanydbpermission) - - [`MSSQL_GrantAnyPermission`](#mssql_grantanypermission) - - [`MSSQL_HasDBScopedCred`](#mssql_hasdbscopedcred) - - [`MSSQL_HasLogin`](#mssql_haslogin) - - [`MSSQL_HasMappedCred`](#mssql_hasmappedcred) - - [`MSSQL_HasProxyCred`](#mssql_hasproxycred) - - [`MSSQL_HostFor`](#mssql_hostfor) - - [`MSSQL_Impersonate`](#mssql_impersonate) - - [`MSSQL_ImpersonateAnyLogin`](#mssql_impersonateanylogin) - - [`MSSQL_IsMappedTo`](#mssql_ismappedto) - - [`MSSQL_IsTrustedBy`](#mssql_istrustedby) - - [`MSSQL_LinkedAsAdmin`](#mssql_linkedasadmin) - - [`MSSQL_LinkedTo`](#mssql_linkedto) - - [`MSSQL_MemberOf`](#mssql_memberof) - - [`MSSQL_Owns`](#mssql_owns) - - [`MSSQL_ServiceAccountFor`](#mssql_serviceaccountfor) - - [`MSSQL_TakeOwnership`](#mssql_takeownership) - -# Overview -Collects BloodHound OpenGraph compatible data from one or more MSSQL servers into individual temporary files, then zips them in the current directory - - Example: `mssql-bloodhound-20250724-115610.zip` - -## System Requirements: - - PowerShell 4.0 or higher - - Target is running SQL Server 2005 or higher - - BloodHound v8.0.0+ with Postgres backend (to use prebuilt Cypher queries): https://bloodhound.specterops.io/get-started/custom-installation#postgresql - -## Minimum Permissions: -### Windows Level: - - Active Directory domain context with line of sight to a domain controller -### MSSQL Server Level: - - **`CONNECT SQL`** (default for new logins) - - **`VIEW ANY DATABASE`** (default for new logins) - -## Recommended Permissions: -### MSSQL Server Level: - - **`VIEW ANY DEFINITION`** permission or `##MS_DefinitionReader##` role membership (available in versions 2022+) - - Needed to read server principals and their permissions - - Without one of these permissions, there will be false negatives (invisible server principals) - - **`VIEW SERVER PERFORMANCE STATE`** permission or `##MSS_ServerPerformanceStateReader##` role membership (available in versions 2022+) or local `Administrators` group privileges on the target (fallback for WMI collection) - - Only used for service account collection - -### MSSQL Database Level: - - **`CONNECT ANY DATABASE`** server permission (available in versions 2014+) or `##MS_DatabaseConnector##` role membership (available in versions 2022+) or login maps to a database user with `CONNECT` on individual databases - - Needed to read database principals and their permissions - - Login maps to **`msdb`** database user with **`db_datareader`** role or with `SELECT` permission on: - - `msdb.dbo.sysproxies` - - `msdb.dbo.sysproxylogin` - - `msdb.dbo.sysproxysubsystem` - - `msdb.dbo.syssubsystems` - - Only used for proxy account collection - -# Usage Info -Run MSSQLHound from a box where you aren’t highly concerned about resource consumption. While there are guardrails in place to stop the script if resource consumption is too high, it’s probably a good idea to be careful and run it on a workstation instead of directly on a critical database server, just in case. - -If you don't already have a specific target or targets in mind, start by running the script with the `-DomainEnumOnly` flag set to see just how many servers you’re dealing with in Active Directory. Then, use the `-ServerInstance` option to run it again for a single server or add all of the servers that look interesting to a file and run it again with the `-ServerListFile` option. - -If you don't do a dry run first and collect from all SQL servers with SPNs in the domain (the default action), expect the script to take a very long time to finish and eat up a ton of disk space if there ar a lot of servers in the environment. Based on limited testing in client environments, the file size for each server before they are all zipped ranges significantly from 2MB to 50MB+, depending on how many objects are on the server. - -To populate the MSSQL node glyphs in BloodHound, execute `MSSQLHound.ps1 -OutputFormat BloodHound-customnodes` (or copy the following) and use the API Explorer page to submit the JSON to the `custom-nodes` endpoint. - -``` -{ - "custom_types": { - "MSSQL_DatabaseUser": { - "icon": { - "name": "user", - "color": "#f5ef42", - "type": "font-awesome" - } - }, - "MSSQL_Login": { - "icon": { - "name": "user-gear", - "color": "#dd42f5", - "type": "font-awesome" - } - }, - "MSSQL_DatabaseRole": { - "icon": { - "name": "users", - "color": "#f5a142", - "type": "font-awesome" - } - }, - "MSSQL_Database": { - "icon": { - "name": "database", - "color": "#f54242", - "type": "font-awesome" - } - }, - "MSSQL_ApplicationRole": { - "icon": { - "name": "robot", - "color": "#6ff542", - "type": "font-awesome" - } - }, - "MSSQL_Server": { - "icon": { - "name": "server", - "color": "#42b9f5", - "type": "font-awesome" - } - }, - "MSSQL_ServerRole": { - "icon": { - "name": "users-gear", - "color": "#6942f5", - "type": "font-awesome" - } - } - } -} -``` - -There are several new edges that have to be non-traversable because they are not abusable 100% of the time, including when: -- the stored AD credentials might be stale/invalid, but maybe they are! - - MSSQL_HasMappedCred - - MSSQL_HasDBScopedCred - - MSSQL_HasProxyCred -- the server principal that owns the database does not have complete control of the server, but maybe it has other interesting permissions - - MSSQL_IsTrustedBy -- the server is linked to another server using a principal that does not have complete control of the remote server, but maybe it has other interesting permissions - - MSSQL_LinkedTo -- the service account can be used to impersonate domain users that have a login to the server, but we don’t have the necessary permissions to check that any domain users have logins - - MSSQL_ServiceAccountFor - - It would be unusual, but not impossible, for the MSSQL Server instance to run in the context of a domain service account and have no logins for domain users. If you can infer that certain domain users have access to a particular MSSQL Server instance or discover that information through other means (e.g., naming conventions, OSINT, organizational documentation, internal communications, etc.), you can request service tickets for those users to the MSSQL Server if you have control of the service account (e.g., by cracking weak passwords for Kerberoastable service principals). - -Want to be a bit more aggressive with your pathfinding queries? You can make these edges traversable using the `-MakeInterestingEdgesTraversable` flag. - -I also recommend conducting a collection with the `-IncludeNontraversableEdges` flag enabled at some point if you need to understand what permissions on which objects allow the traversable edges to be created. By default, non-traversable edges are skipped to make querying the data for valid attack paths easier. This is still a work in progress, but look out for the “Composition” item in the edge entity panel for each traversable edges to grab a pastable cypher query to identify the offending permissions. - -If the [prebuilt Cypher queries](saved_queries) are returning `failed to translate kinds: unable to map kinds:` errors, upload [seed_data.json](seed_data.json) to populate a single fake instance of each new edge class so they can be queried. - -# Command Line Options -For the latest and most reliable information, please execute MSSQLHound with the `-Help` flag. - -| Option
______________________________________________ | Values
_______________________________________________________________________________________________ | -|--------|--------| -| **-Help** `` | • Display usage information | -| **-OutputFormat** `` | • **BloodHound**: OpenGraph implementation that collects data in separate files for each MSSQL server, then zips them up and deletes the originals. The zip can be uploaded to BloodHound by navigating to `Administration` > `File Ingest`
• **BloodHound-customnodes**: Generate JSON to POST to `custom-nodes` API endpoint
• **BloodHound-customnode**: Generate JSON for DELETE on `custom-nodes` API endpoint
• **BHGeneric**: Work in progress to make script compatible with [BHOperator](https://github.com/SadProcessor/BloodHoundOperator) | -| **-ServerInstance** `` | • A specific MSSQL instance to collect from:
    • **Null**: Query the domain for SPNs and collect from each server found
    • **Name/FQDN**: ``
    • **Instance**: `[:\|:]`
    • **SPN**: `/[:\|:]` | -| **-ServerListFile** `` | • Specify the path to a file containing multiple server instances to collect from in the ServerInstance formats above | -| **-ServerList** `` | • Specify a comma-separated list of server instances to collect from in the ServerInstance formats above | -| **-TempDir** `` | • Specify the path to a temporary directory where .json files will be stored before being zipped
Default: new directory created with `[System.IO.Path]::GetTempPath()` | -| **-ZipDir** `` | • Specify the path to a directory where the final .zip file will be stored
• Default: current directory | -| **-MemoryThresholdPercent** `` | • Maximum memory allocation limit, after which the script will exit to prevent availability issues
• Default: `90` | -| **-Credential** `` | • Specify a PSCredential object to connect to the remote server(s) | -| **-UserID** `` | • Specify a **login** to connect to the remote server(s) | -| **-SecureString** `` | • Specify a SecureString object for the login used to connect to the remote server(s) | -| **-Password** `` | • Specify a **password** for the login used to connect to the remote server(s) | -| **-Domain** `` | • Specify a **domain** to use for name and SID resolution | -| **-DomainController** `` | • Specify a **domain controller** FQDN/IP to use for name and SID resolution | -| **-IncludeNontraversableEdges** (switch) | • **On**: • Collect both **traversable and non-traversable edges**
• **Off (default)**: Collect **only traversable edges** (good for offensive engagements until Pathfinding supports OpenGraph edges) | -| **-MakeInterestingEdgesTraversable** (switch) | • **On**: Make the following edges traversable (useful for offensive engagements but prone to false positive edges that may not be abusable):
    • **MSSQL_HasDBScopedCred**
    • **MSSQL_HasMappedCred**
    • **MSSQL_HasProxyCred**
    • **MSSQL_IsTrustedBy**
    • **MSSQL_LinkedTo**
    • **MSSQL_ServiceAccountFor**
• **Off (default)**: The edges above are non-traversable | -| **-SkipLinkedServerEnum** (switch) | • **On**: Don't enumerate linked servers
• **Off (default)**: Enumerate linked servers | -| **-CollectFromLinkedServers** (switch) | • **On**: If linked servers are found, try and perform a full MSSQL collection against each server
• **Off (default)**: If linked servers are found, **don't** try and perform a full MSSQL collection against each server | -| **-DomainEnumOnly** (switch) | • **On**: If SPNs are found, **don't** try and perform a full MSSQL collection against each server
• **Off (default)**: If SPNs are found, try and perform a full MSSQL collection against each server | -| **-InstallADModule** (switch) | • **On**: Try to install the ActiveDirectory module for PowerShell if it is not already installed
• **Off (default)**: Do not try to install the ActiveDirectory module for PowerShell if it is not already installed. Rely on DirectoryServices, ADSISearcher, DirectorySearcher, and NTAccount.Translate() for object resolution. | -| **-LinkedServerTimeout** `` | • Give up enumerating linked servers after `X` seconds
• Default: `300` seconds (5 minutes) | -| **-FileSizeLimit** `` | • Stop enumeration after all collected files exceed this size on disk
• Supports MB, GB
• Default: `1GB` | -| **-FileSizeUpdateInterval** `` | • Receive periodic size updates as files are being written for each server
• Default: `5` seconds | -| **-Version** `` | • Display version information and exit | - -# Limitations -- MSSQLHound can’t currently collect nodes and edges from linked servers over the link, although I’d like to add more linked server collection functionality in the future. -- MSSQLHound doesn’t check DENY permissions. Because permissions are denied by default unless explicitly granted, it is assumed that use of DENY permissions is rare. One exception is the CONNECT SQL permission, for which the DENY permission is checked to see if the principal can remotely log in to the MSSQL instance at all. -- MSSQLHound stops enumerating at the database level. It could be modified to go deeper (to the table/stored procedure or even column level), but that would degrade performance, especially when merging with the AD graph. -- EPA enumeration without a login or Remote Registry access is not yet supported (but will be soon) -- Separate collections in domains that can’t reach each other for principal SID resolution may not merge correctly when they are ingested (i.e., more than one MSSQL_Server node may represent the same server, one labelled with the SID, one with the name). - -# Future Development: -- Unprivileged EPA collection (in the works) -- Option to zip after every server (to save disk space) -- Collection from linked servers -- Collect across domains and trusts -- Azure extension for SQL Server -- AZUser/Groups for server logins / database users -- Cross database ownership chaining -- DENY permissions -- EXECUTE permission on xp_cmdshell -- UNSAFE/EXTERNAL_ACCESS permission on assembly (impacted by TRUSTWORTHY) -- Add this to CoerceAndRelayToMSSQL: - - Domain principal has CONNECT SQL (and EXECUTE on xp_dirtree or other stored procedures that will authenticate to a remote host) - - Service account/Computer has a server login that is enabled on another SQL instance - - EPA is not required on remote SQL instance - -# MSSQL Graph Model -MSSQL Red Green (1) - -# MSSQL Nodes Reference -## Server Level -### Server Instance (`MSSQL_Server` node) -image
-The entire installation of the MSSQL Server database management system (DBMS) that contains multiple databases and server-level objects - -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|----------|------------| -| **Label**: string | • Format: `[:\|:]`
• Examples:
    • `SQL.MAYYHEM.COM` (default port and instance name)
    • `SQL.MAYYHEM.COM:SQL2012` (named instance) | -| **Object ID**: string | • Format: `:`
• Example: `S-1-5-21-843997178-3776366836-1907643539-1108:1433`
• Port or instance name should be a part of the identifier in case there are multiple MSSQL Server instances on the same host.
• Two or more accounts are permitted to have identical SPNs in Active Directory (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/setspn), and two or more names may resolve to the same host (e.g., `MSSQLSvc/ps1-db:1433` and `MSSQLSvc/ps1-db.mayyhem.com:1433`) so we will use the domain SID instead of the host portion of the SPN, when available.
• MSSQLSvc SPNs may contain an instance name instead of the port, in which case the SQL Browser service (`UDP/1434`) is used to determine the listening port for the MSSQL server. In other cases the port is dynamically chosen and the SPN updated when the service [re]starts. The `ObjectIdentifier` must be capable of storing either value in case there is an instance name in the SPN and the SQL Browser service is not reachable, and prefer instance over port.
• The script currently falls back to using the FQDN instead of the SID if the server can't be resolved to a domain object (for example, if it is resolved via DNS or reachable via the MSSQL port but can't be resolved to a principal in another domain).
    • This format complicates things when trying to merge objects from collections taken from different domains, with different privileges, or when servers are discovered via SQL links. For example, when collecting from `hostA.domain1.local`, a link to `hostB.domain2.local:1433` is discovered. The collector can't resolve principals in `domain2`, so its `ObjectIdentifier` is the `hostname:port` instead. However, `hostB.domain2.local` is reachable on port `1433` and after connecting, the collector determines that its instance name is `SQLHOSTB`. Later, a collection is done on `HostB` from within `domain2`, so its `ObjectIdentifier` is either `sid:port` or `sid:instanceName`, depending on what's in the SPNs.| -| **Databases**: List\ | • Names of databases contained in the SQL Server instance | -| **Extended Protection**: string
(`Off` \| `Allowed` \| `Required` \| `Allowed/Required`) |• Allowed and required both prevent authentication relay to MSSQL (using service binding if Force Encryption is `No`, using channel binding if Force Encryption is `Yes`). | -| **Force Encryption**: string
(`No` \| `Yes`) | • Does the server require clients to encrypt communications? | -| **Has Links From Servers**: List\ | • SQL Server instances that have a link to this SQL Server instance
• There is no way to view this using SSMS or other native tools on the target of a link. | -| **Instance Name**: string | • SQL Server instances are identified using either a port or an instance name.
• Default: `MSSQLSERVER` | -| **Is Any Domain Principal Sysadmin**: bool | • If a domain principal is a member of the sysadmin server role or has equivalent permissions (`securityadmin`, `CONTROL SERVER`, or `IMPERSONATE ANY LOGIN`), the domain service account running MSSQL can impersonate such a principal to gain control of the server via S4U2Silver. See the `MSSQL_GetAdminTGS` edge for more information. | -| **Is Linked Server Target**: bool | • Does any SQL Server instance have a link to this SQL Server instance?
• There is no way to view this using SSMS or other native tools on the target of a link. | -| **Is Mixed Mode Auth Enabled**: bool | • **True**: both Windows and SQL logins are permitted to access the server remotely
• **False**: only Windows logins are permitted to access the server remotely | -| **Linked To Servers**: List\ | • SQL Server instances that this SQL Server instance is linked to | -| **Port**: uint |• SQL Server instances are identified using either a port or an instance name.
• Default: `1433` | -| **Service Account**: string | • The Windows account running the SQL Server instance | -| **Service Principal Names**: List\ | • SPNs associated with this SQL Server instance | -| **Version**: string | • Result of `SELECT @@VERSION` - -### Server Login (`MSSQL_Login` node) -image
-A type of server principal that can be assigned permissions to access server-level objects, such as the ability to connect to the instance or modify server role membership. These principals can be local to the instance (SQL logins) or mapped to a domain user, computer, or group (Windows logins). Server logins can be added as members of server roles to inherit the permissions assigned to the role. - -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|----------|------------| -| **Label**: string | • Format: ``
• Example: `MAYYHEM\sqladmin` | -| **Object ID**: string | • Format: `@`
• Example: `MAYYHEM\sqladmin@S-1-5-21-843997178-3776366836-1907643539-1108:1433` | -| **Active Directory Principal**: string | • Name of the AD principal this login is mapped to | -| **Active Directory SID**: string | • SID of the AD principal this login is mapped to | -| **Create Date**: datetime | • When the login was created | -| **Database Users**: List\ | • Names of each database user this login is mapped to | -| **Default Database**: string | • The default database used when the login connects to the server | -| **Disabled**: bool | • Is the account disabled? | -| **Explicit Permissions**: List\ | • Server level permissions assigned directly to this login
• Does not include all effective permissions such as those granted through role membership | -| **Is Active Directory Principal**: bool | • If a domain principal has a login, the domain service account running MSSQL can impersonate such a principal to gain control of the login via S4U2Silver. | -| **Member of Roles**: List\ | • Names of roles this principal is a direct member of
• Does not include nested memberships | -| **Modify Date**: datetime | • When the principal was last modified | -| **Principal Id**: uint | • The identifier the SQL Server instance uses to associate permissions and other objects with this principal | -| **SQL Server**: string | • Name of the SQL Server where this object is a principal | -| **Type**: string | • **ASYMMETRIC_KEY_MAPPED_LOGIN**: Used to sign modules within the database, such as stored procedures, functions, triggers, or assemblies and can't be used to connect to the server remotely. I haven't messed with these much but they can be assigned permissions and impersonated.
• **CERTIFICATE_MAPPED_LOGIN**: Used to sign modules within the database, such as stored procedures, functions, triggers, or assemblies and can't be used to connect to the server remotely. I haven't messed with these much but they can be assigned permissions and impersonated.
• **SQL_LOGIN**: This login is local to the SQL Server instance and mixed-mode authentication must be enabled to connect with it
• **WINDOWS_LOGIN**: A Windows account is mapped to this login
• **WINDOWS_GROUP**: A Windows group is mapped to this login | - -### Server Role (`MSSQL_ServerRole` node) -image
-A type of server principal that can be assigned permissions to access server-level objects, such as the ability to connect to the instance or modify server role membership. Server logins and user-defined server roles can be added as members of server roles, inheriting the role's permissions. - -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|----------|------------| -| **Label**: string | • Format: ``
• Example: `processadmin` | -| **Object ID**: string | • Format: `@`
• Example: `processadmin@S-1-5-21-843997178-3776366836-1907643539-1108:1433` | -| **Create Date**: datetime | • When the role was created | -| **Explicit Permissions**: List\ | • Server level permissions assigned directly to this login
• Does not include all effective permissions such as those granted through role membership | -| **Is Fixed Role**: bool | • Whether or not the role is built-in (i.e., ships with MSSQL and can't be removed) | -| **Member of Roles**: List\ | • Names of roles this principal is a direct member of
• Does not include nested memberships | -| **Members**: List\ | • Names of each principal that is a direct member of this role | -| **Modify Date**: datetime | • When the principal was last modified | -| **Principal Id**: uint | • The identifier the SQL Server instance uses to associate permissions and other objects with this principal | -| **SQL Server**: string | • Name of the SQL Server where this object is a principal | - -## Database Level - -### Database (`MSSQL_Database` node) -image
-A collection of database principals (e.g., users and roles) as well as object groups called schemas, each of which contains securable database objects such as tables, views, and stored procedures. - -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|----------|------------| -| **Label**: string | • Format: ``
• Example: `master` | -| **Object ID**: string | • Format: `\`
• Example: `S-1-5-21-843997178-3776366836-1907643539-1108:1433\master` | -| **Is Trustworthy**: bool | • Is the `Trustworthy` property of this database set to `True`?
• When `Trustworthy` is `True`, principals with control of the database are permitted to execute server level actions in the context of the database's owner, allowing server compromise if the owner has administrative privileges.
• Example: If `sa` owns the `CM_PS1` database and the database's `Trustworthy` property is `True`, then a user in the database with sufficient privileges could create a stored procedure with the `EXECUTE AS OWNER` statement and leverage the `sa` account's permissions to execute SQL statements on the server. See the `MSSQL_ExecuteAsOwner` edge for more information. | -| **Owner Login Name**: string | • Example: `MAYYHEM\cthompson` | -| **Owner Principal ID**: uint | • The identifier the SQL Server instance uses to associate permissions and other objects with this principal | -| **SQL Server**: string | • Name of the SQL Server where this object is a principal | - -### Database User (`MSSQL_DatabaseUser` node) -image
-A user that has access to the specific database it is contained in. Users may be mapped to a login or may be created without a login. Users can be assigned permissions to access database-level objects, such as the ability to connect to the database, access tables, modify database role membership, or execute stored procedures. Users and user-defined database roles can be added as members of database roles, inheriting the role's permissions. - -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|----------|------------| -| **Label**: string | • Format: `@`
• Example: `MAYYHEM\LOWPRIV@CM_CAS` | -| **Object ID**: string | • Format: `@`
• `Example: MAYYHEM\LOWPRIV@S-1-5-21-843997178-3776366836-1907643539-1117:1433\CM_CAS` | -| **Create Date**: datetime | • When the user was created | -| **Database**: string | • Name of the database where this user is a principal | -| **Default Schema**: string | • The default schema used when the user connects to the database | -| **Explicit Permissions**: List\ | • Database level permissions assigned directly to this principal
• Does not include all effective permissions such as those granted through role membership | -| **Member of Roles**: List\ | • Names of roles this principal is a direct member of
• Does not include nested memberships | -| **Modify Date**: datetime | • When the principal was last modified | -| **Principal Id**: uint | • The identifier the SQL Server instance uses to associate permissions and other objects with this principal | -| **Server Login**: string | • Name of the login this user is mapped to | -| **SQL Server**: string | • Name of the SQL Server where this object is a principal | -| **Type**: string | • **ASYMMETRIC_KEY_MAPPED_USER**: Used to sign modules within the database, such as stored procedures, functions, triggers, or assemblies and can't be used to connect to the server remotely. I haven't messed with these much but they can be assigned permissions and impersonated.
• **CERTIFICATE_MAPPED_USER**: Used to sign modules within the database, such as stored procedures, functions, triggers, or assemblies and can't be used to connect to the server remotely. I haven't messed with these much but they can be assigned permissions and impersonated.
• **SQL_USER**: This user is local to the SQL Server instance and mixed-mode authentication must be enabled to connect with it
• **WINDOWS_USER**: A Windows account is mapped to this user
• **WINDOWS_GROUP**: A Windows group is mapped to this user | - -### Database Role (`MSSQL_DatabaseRole` node) -image
-A type of database principal that can be assigned permissions to access database-level objects, such as the ability to connect to the database, access tables, modify database role membership, or execute stored procedures. Database users, user-defined database roles, and application roles can be added as members of database roles, inheriting the role's permissions. - -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|----------|------------| -| **Label**: string | • Format: `@`
• Example: `db_owner@CM_CAS` | -| **Object ID**: string | • Format: `@`
• Example: `db_owner@S-1-5-21-843997178-3776366836-1907643539-1117:1433\CM_CAS` | -| **Create Date**: datetime | • When the role was created | -| **Database**: string | • Name of the database where this role is a principal | -| **Explicit Permissions**: List\ | • Database level permissions assigned directly to this principal
• Does not include all effective permissions such as those granted through role membership | -| **Member of Roles**: List\ | • Names of roles this principal is a direct member of
• Does not include nested memberships | -| **Members**: List\ | • Names of each principal that is a direct member of this role | -| **Modify Date**: datetime | • When the principal was last modified | -| **Principal Id**: uint | • The identifier the SQL Server instance uses to associate permissions and other objects with this principal | -| **SQL Server**: string | • Name of the SQL Server where this object is a principal | - -### Application Role (`MSSQL_ApplicationRole` node) -image
-A type of database principal that is not associated with a user but instead is activated by an application using a password so it can interact with the database using the role's permissions. - -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|----------|------------| -| **Label**: string | • Format: `@`
• Example: `TESTAPPROLE@TESTDATABASE` | -| **Object ID**: string | • Format: `@`
• Example: `TESTAPPROLE@S-1-5-21-843997178-3776366836-1907643539-1108:1433\TESTDATABASE` | -| **Create Date**: datetime | • When the principal was created | -| **Database**: string | • Name of the database where this object is a principal | -| **Default Schema**: string | • The default schema used when the principal connects to the database | -| **Explicit Permissions**: List\ | • Database level permissions assigned directly to this principal
• Does not include all effective permissions such as those granted through role membership | -| **Member of Roles**: List\ | • Names of roles this principal is a direct member of
• Does not include nested memberships | -| **Modify Date**: datetime | • When the principal was last modified | -| **Principal Id**: uint | • The identifier the SQL Server instance uses to associate permissions and other objects with this principal | -| **SQL Server**: string | • Name of the SQL Server where this object is a principal | - - -# MSSQL Edges Reference -This section includes explanations for edges that have their own unique properties. Please refer to the `$script:EdgePropertyGenerators` variable in `MSSQLHound.ps1` for the following details: -- Source and target node classes (all combinations) -- Requirements -- Default fixed roles with the permission -- Traversability -- Entity panel details (dynamically-generated) - - General - - Windows Abuse - - Linux Abuse - - OPSEC - - References - - Composition Cypher (where applicable) - -## Edge Classes and Properties - -### `MSSQL_ExecuteAsOwner` -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|-----------------------------------------------|------------| -| **Database**: string | • Name of the target database where the source can execute SQL statements as the server-level owning principal | -| **Database Is Trustworthy**: bool | • **True**: Database principals that can execute `EXECUTE AS OWNER` statements can execute actions in the context of the server principal that owns the database
• **False**: The database isn't allowed to access resources beyond the scope of the database | -| **Owner Has Control Server**: bool | • **True**: The server principal that owns the database has the `CONTROL SERVER` permission, allowing complete control of the MSSQL server instance. | -| **Owner Has Impersonate Any Login**: bool | • **True**: The server principal that owns the database has the `IMPERSONATE ANY LOGIN` permission, allowing complete control of the MSSQL server instance. | -| **Owner Has Securityadmin**: bool | • **True**: The server principal that owns the database is a member of the `securityadmin` server role, allowing complete control of the MSSQL server instance. | -| **Owner Has Sysadmin**: bool | • **True**: The server principal that owns the database is a member of the `sysadmin` server role, allowing complete control of the MSSQL server instance. | -| **Owner Login Name**: string | • The name of the server login that owns the database
• Example: `MAYYHEM\cthompson` | -| **Owner Object Identifier**: string | • The object identifier of the server login that owns the database | -| **Owner Principal ID**: uint | • The identifier the SQL Server instance uses to associate permissions and other objects with this principal | -| **SQL Server**: string | • Name of the SQL Server where this object is a principal | - -### `MSSQL_GetAdminTGS` -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|-----------------------------------------------|------------| -| **Domain Principals with ControlServer**: List | • Domain principals with logins that have the `CONTROL SERVER` effective permission, allowing complete control of the MSSQL server instance. | -| **Domain Principals with ImpersonateAnyLogin**: List | • Domain principals with logins that have the `IMPERSONATE ANY LOGIN` effective permission, allowing complete control of the MSSQL server instance. | -| **Domain Principals with Securityadmin**: List | • Domain principals with membership in the `securityadmin` server role, allowing complete control of the MSSQL server instance. | -| **Domain Principals with Sysadmin**: List | • Domain principals with membership in the `sysadmin` server role, allowing complete control of the MSSQL server instance. | - -### `MSSQL_HasDBScopedCred` -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|-----------------------------------------------|------------| -| **Credential ID**: string | • The identifier the SQL Server instance uses to associate other objects with this principal | -| **Credential Identity**: string | • The domain principal this credential uses to authenticate to resources | -| **Credential Name**: string | • The name used to identify this credential in the SQL Server instance | -| **Create Date**: datetime | • When the credential was created | -| **Database**: string | • Name of the database where this object is a credential | -| **Modify Date**: datetime | • When the credential was last modified | -| **Resolved SID**: string | • The domain SID for the credential identity | - -### `MSSQL_HasMappedCred` -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|-----------------------------------------------|------------| -| **Credential ID**: uint | • The identifier the SQL Server instance uses to associate other objects with this principal | -| **Credential Identity**: string | • The domain principal this credential uses to authenticate to resources | -| **Credential Name**: string | • The name used to identify this credential in the SQL Server instance | -| **Create Date**: datetime | • When the credential was created | -| **Modify Date**: datetime | • When the credential was last modified | -| **Resolved SID**: string | • The domain SID for the credential identity | - -### `MSSQL_HasProxyCred` -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|-----------------------------------------------|------------| -| **Authorized Principals**: List | • Principals that are authorized to use this proxy credential | -| **Credential ID**: string | • The identifier the SQL Server instance uses to associate other objects with this principal | -| **Credential Identity**: string | • The domain principal this credential uses to authenticate to resources | -| **Credential Name**: string | • The name used to identify this credential in the SQL Server instance | -| **Description**: string | • User-provided description of the proxy that uses this credential | -| **Is Enabled**: bool | • Is the proxy that uses this credential enabled? | -| **Proxy ID**: uint | • The identifier the SQL Server instance uses to associate other objects with this proxy | -| **Proxy Name**: string | • The name used to identify this proxy in the SQL Server instance | -| **Resolved SID**: string | • The domain SID for the credential identity | -| **Resolved Type**: string | • The class of domain principal for the credential identity | -| **Subsystems**: List | • Subsystems this proxy is configured with (e.g., `CmdExec`, `PowerShell`) | - -### `MSSQL_LinkedAsAdmin` -| Property
______________________________________________ | Definition
_______________________________________________________________________________________________ | -|-----------------------------------------------|------------| -| **Data Access**: bool | • **True (enabled)**:
    • The linked server can be used in distributed queries
    • You can `SELECT`, `INSERT`, `UPDATE`, `DELETE` data through the linked server
    • Four-part naming queries work: `[LinkedServer].[Database].[Schema].[Table]`
    • `OPENQUERY()` statements work against this linked server
• **False (disabled)**:
    • The linked server connection still exists but cannot be used for data queries
    • Attempts to query through it will fail with an error
  • The linked server can still be used for other purposes like RPC calls (if RPC is enabled) | -| **Data Source**: string | • Format: `[\instancename]`
• Examples: `SITE-DB` or `CAS-PSS\CAS` | -| **Local Login**: List | • The login(s) on the source that can use the link and connect to the linked server using the Remote Login | -| **Path**: string | • The link used to collect the information needed to create this edge | -| **Product**: string | • A user-defined name of the product used by the remote server
• Examples: `SQL Server`, `Oracle`, `Access` | -| **Provider**: string | • The driver or interface that SQL Server uses to communicate with the remote data source | -| **Remote Current Login**: string | • Displays the login context that is actually used on the remote linked server based on the results of the `SELECT SYSTEM_USER` SQL statement on the remote linked server
• If impersonation is used, it is likely that this value will be the login used for collection
• If not, this should match Remote Login | -| **Remote Has Control Server**: bool | • Does the login context on the remote server have the `CONTROL SERVER` permission? | -| **Remote Has Impersonate Any Login**: bool | • Does the login context on the remote server have the `IMPERSONATE ANY LOGIN` permission? | -| **Remote Is Mixed Mode**: bool | • Is mixed mode authentication (for both Windows and SQL logins) enabled on the remote server? | -| **Remote Is Securityadmin**: bool | • Is the login context on the remote server a member of the `securityadmin` server role? | -| **Remote Is Sysadmin**: bool | • Is the login context on the remote server a member of the `sysadmin` server role? | -| **Remote Login**: string | • The SQL Server authentication login that exists on the remote server that connections over this link are mapped to
• The password for this login must be saved on the source server
• Will be null if impersonation is used, in which case the login context being used on the source server is used to connect to the remote linked server | -| **Remote Server Roles**: List | • Server roles the remote login context is a member of | -| **RPC Out**: bool | • Can the source server call stored procedures on remote server? | -| **Uses Impersonation**: bool | • Does the linked server attempt to use the current user's Windows credentials to authenticate to the remote server?
• For SQL Server authentication, a login with the exact same name and password must exist on the remote server.
• For Windows logins, the login must be a valid login on the linked server.
• This requires Kerberos delegation to be properly configured
• The user's actual Windows identity is passed through to the remote server | - -### Remaining Edges -Please refer to the `$script:EdgePropertyGenerators` variable in `MSSQLHound.ps1` for the following details: -- Source and target node classes (all combinations) -- Requirements -- Default fixed roles with the permission -- Traversability -- Entity panel details (dynamically-generated) - - General - - Windows Abuse - - Linux Abuse - - OPSEC - - References - - Composition Cypher (where applicable) - -All edges based on permissions may contain the `With Grant` property, which means the source not only has the permission but can grant it to other principals. - -| Edge Class
______________________________________________ | Properties
_______________________________________________________________________________________________ | -|-----------------------------------------------|------------| - -| **`CoerceAndRelayToMSSQL`** | • No unique edge properties | - -| **`MSSQL_AddMember`** | • No unique edge properties | - -| **`MSSQL_Alter`** | • No unique edge properties | - -| **`MSSQL_AlterAnyAppRole`** | • No unique edge properties | - -| **`MSSQL_AlterAnyDBRole`** | • No unique edge properties | - -| **`MSSQL_AlterAnyLogin`** | • No unique edge properties | - -| **`MSSQL_AlterAnyServerRole`** | • No unique edge properties | - -| **`MSSQL_ChangeOwner`** | • No unique edge properties | - -| **`MSSQL_ChangePassword`** | • No unique edge properties | - -| **`MSSQL_Connect`** | • No unique edge properties | - -| **`MSSQL_ConnectAnyDatabase`** | • No unique edge properties | - -| **`MSSQL_Contains`** | • No unique edge properties | - -| **`MSSQL_Control`** | • No unique edge properties | - -| **`MSSQL_ControlDB`** | • No unique edge properties | - -| **`MSSQL_ControlServer`** | • No unique edge properties | - -| **`MSSQL_ExecuteAs`** | • No unique edge properties | - -| **`MSSQL_ExecuteOnHost`** | • No unique edge properties | - -| **`MSSQL_GetTGS`** | • No unique edge properties | - -| **`MSSQL_GrantAnyDBPermission`** | • No unique edge properties | - -| **`MSSQL_GrantAnyPermission`** | • No unique edge properties | - -| **`MSSQL_HasLogin`** | • No unique edge properties | - -| **`MSSQL_HostFor`** | • No unique edge properties | - -| **`MSSQL_Impersonate`** | • No unique edge properties | - -| **`MSSQL_ImpersonateAnyLogin`** | • No unique edge properties | - -| **`MSSQL_IsMappedTo`** | • No unique edge properties | - -| **`MSSQL_IsTrustedBy`** | • No unique edge properties | - -| **`MSSQL_LinkedTo`** | • Edge properties are the same as `MSSQL_LinkedAsAdmin` | - -| **`MSSQL_MemberOf`** | • No unique edge properties | - -| **`MSSQL_Owns`** | • No unique edge properties | - -| **`MSSQL_ServiceAccountFor`** | • No unique edge properties | - -| **`MSSQL_TakeOwnership`** | • No unique edge properties | +# MSSQLHound +image + +A collector for adding MSSQL attack paths to [BloodHound](https://github.com/SpecterOps/BloodHound) with [OpenGraph](https://specterops.io/opengraph) by Chris Thompson at [SpecterOps](https://x.com/SpecterOps). Available as both a PowerShell script and a cross-platform Go binary (with concurrent collection, SOCKS5 proxy support, and streaming output). + +Introductory blog posts: +- https://specterops.io/blog/2025/08/04/adding-mssql-to-bloodhound-with-opengraph/ +- https://specterops.io/blog/2026/01/20/updates-to-the-mssqlhound-opengraph-collector-for-bloodhound/ +- https://specterops.io/blog/2026/04/23/mssqlhound-now-available-in-go/ + +Please hit me up on the [BloodHound Slack](http://ghst.ly/BHSlack) (@Mayyhem), Twitter ([@_Mayyhem](https://x.com/_Mayyhem)), or open an issue if you have any questions I can help with! + +# Table of Contents + +- [Overview](#overview) + - [System Requirements](#system-requirements) + - [Minimum Permissions](#minimum-permissions) + - [Recommended Permissions](#recommended-permissions) +- [OPSEC](#opsec) + - [Network Connections](#network-connections) + - [Authentication Events](#authentication-events) + - [Subprocesses Executed](#subprocesses-executed) + - [SQL Queries Executed on Targets](#sql-queries-executed-on-targets) + - [Files Created](#files-created) +- [Go Version](#go-version) + - [Why a Go Port?](#why-a-go-port) + - [Building](#building) + - [Go Usage](#go-usage) + - [Basic Usage](#basic-usage) + - [Multiple Servers](#multiple-servers) + - [Full Domain Enumeration](#full-domain-enumeration) + - [DNS and Domain Controller Configuration](#dns-and-domain-controller-configuration) + - [SOCKS5 Proxy Support](#socks5-proxy-support) + - [Credential Fallback](#credential-fallback) + - [Kerberos Authentication](#kerberos-authentication) + - [Pass-the-Hash](#pass-the-hash) + - [Domain Enum Only (Reconnaissance)](#domain-enum-only-reconnaissance) + - [Output and Storage Options](#output-and-storage-options) + - [BloodHound Upload](#bloodhound-upload) + - [Interesting Edge Options](#interesting-edge-options) + - [Linked Server Options](#linked-server-options) + - [test-epa-matrix Subcommand](#test-epa-matrix-subcommand) + - [Shell Completion](#shell-completion) + - [Go Command Line Options](#go-command-line-options) + - [Key Differences from PowerShell Version](#key-differences-from-powershell-version) + - [CVE Detection](#cve-detection) + - [Known Limitations and Issues (Go)](#known-limitations-and-issues-go) + - [Troubleshooting (Go)](#troubleshooting-go) +- [PowerShell Usage](#powershell-usage) +- [PowerShell Command Line Options](#powershell-command-line-options) +- [Limitations](#limitations) +- [Future Development](#future-development) +- [Credits](#credits) +- [MSSQL Graph Model](#mssql-graph-model) +- [MSSQL Nodes Reference](#mssql-nodes-reference) + - [Server Level](#server-level) + - [`MSSQL_Server`](#server-instance-mssql_server-node) + - [`MSSQL_Login`](#server-login-mssql_login-node) + - [`MSSQL_ServerRole`](#server-role-mssql_serverrole-node) + - [Database Level](#database-level) + - [`MSSQL_Database`](#database-mssql_database-node) + - [`MSSQL_DatabaseUser`](#database-user-mssql_databaseuser-node) + - [`MSSQL_DatabaseRole`](#database-role-mssql_databaserole-node) + - [`MSSQL_ApplicationRole`](#application-role-mssql_applicationrole-node) +- [MSSQL Edges Reference](#mssql-edges-reference) + - [Edge Classes and Properties](#edge-classes-and-properties) + - [`MSSQL_CoerceAndRelayToMSSQL`](#mssql_coerceandrelaytomssql) + - [`MSSQL_AddMember`](#mssql_addmember) + - [`MSSQL_Alter`](#mssql_alter) + - [`MSSQL_AlterAnyAppRole`](#mssql_alteranyapprole) + - [`MSSQL_AlterAnyDBRole`](#mssql_alteranydbrole) + - [`MSSQL_AlterAnyLogin`](#mssql_alteranylogin) + - [`MSSQL_AlterAnyServerRole`](#mssql_alteranyserverrole) + - [`MSSQL_ChangeOwner`](#mssql_changeowner) + - [`MSSQL_ChangePassword`](#mssql_changepassword) + - [`MSSQL_Connect`](#mssql_connect) + - [`MSSQL_ConnectAnyDatabase`](#mssql_connectanydatabase) + - [`MSSQL_Contains`](#mssql_contains) + - [`MSSQL_Control`](#mssql_control) + - [`MSSQL_ControlDB`](#mssql_controldb) + - [`MSSQL_ControlServer`](#mssql_controlserver) + - [`MSSQL_ExecuteAs`](#mssql_executeas) + - [`MSSQL_ExecuteAsOwner`](#mssql_executeasowner) + - [`MSSQL_ExecuteOnHost`](#mssql_executeonhost) + - [`MSSQL_GetAdminTGS`](#mssql_getadmintgs) + - [`MSSQL_GetTGS`](#mssql_gettgs) + - [`MSSQL_GrantAnyDBPermission`](#mssql_grantanydbpermission) + - [`MSSQL_GrantAnyPermission`](#mssql_grantanypermission) + - [`MSSQL_HasDBScopedCred`](#mssql_hasdbscopedcred) + - [`MSSQL_HasLogin`](#mssql_haslogin) + - [`MSSQL_HasMappedCred`](#mssql_hasmappedcred) + - [`MSSQL_HasProxyCred`](#mssql_hasproxycred) + - [`MSSQL_HostFor`](#mssql_hostfor) + - [`MSSQL_Impersonate`](#mssql_impersonate) + - [`MSSQL_ImpersonateAnyLogin`](#mssql_impersonateanylogin) + - [`MSSQL_IsMappedTo`](#mssql_ismappedto) + - [`MSSQL_IsTrustedBy`](#mssql_istrustedby) + - [`MSSQL_LinkedAsAdmin`](#mssql_linkedasadmin) + - [`MSSQL_LinkedTo`](#mssql_linkedto) + - [`MSSQL_MemberOf`](#mssql_memberof) + - [`MSSQL_Owns`](#mssql_owns) + - [`MSSQL_ServiceAccountFor`](#mssql_serviceaccountfor) + - [`MSSQL_TakeOwnership`](#mssql_takeownership) + +# Overview +Collects BloodHound OpenGraph compatible data from one or more MSSQL servers into individual temporary files, then zips them in the current directory + - Example: `mssql-bloodhound-20250724-115610.zip` + +## System Requirements: + - PowerShell 4.0 or higher + - Target is running SQL Server 2005 or higher + - BloodHound v8.0.0+ with Postgres backend (to use prebuilt Cypher queries): https://bloodhound.specterops.io/get-started/custom-installation#postgresql + - **For Kerberos authentication (`-k`):** `krb5-user` package on Linux (`sudo apt install krb5-user`) + +## Minimum Permissions: +### Windows Level: + - Active Directory domain context with line of sight to a domain controller +### MSSQL Server Level: + - **`CONNECT SQL`** (default for new logins) + - **`VIEW ANY DATABASE`** (default for new logins) + +## Recommended Permissions: +### MSSQL Server Level: + - **`VIEW ANY DEFINITION`** permission or `##MS_DefinitionReader##` role membership (available in versions 2022+) + - Needed to read server principals and their permissions + - Without one of these permissions, there will be false negatives (invisible server principals) + - **`VIEW SERVER PERFORMANCE STATE`** permission or `##MSS_ServerPerformanceStateReader##` role membership (available in versions 2022+) or local `Administrators` group privileges on the target (fallback for WMI collection) + - Only used for service account collection + +### MSSQL Database Level: + - **`CONNECT ANY DATABASE`** server permission (available in versions 2014+) or `##MS_DatabaseConnector##` role membership (available in versions 2022+) or login maps to a database user with `CONNECT` on individual databases + - Needed to read database principals and their permissions + - Login maps to **`msdb`** database user with **`db_datareader`** role or with `SELECT` permission on: + - `msdb.dbo.sysproxies` + - `msdb.dbo.sysproxylogin` + - `msdb.dbo.sysproxysubsystem` + - `msdb.dbo.syssubsystems` + - Only used for proxy account collection + +# OPSEC + +This section documents every network connection, authentication event, subprocess, query, and file artifact produced by MSSQLHound so operators can make informed decisions about detection risk. + +## Network Connections + +All TCP connections support SOCKS5 proxy tunneling (`--proxy`). TLS connections use `InsecureSkipVerify=true` and cap at TLS 1.2. + +| Protocol | Port | Transport | Target | Purpose | Conditions | +|----------|------|-----------|--------|---------|------------| +| TDS (SQL Server) | 1433/tcp (default, configurable) | TCP with optional TLS | Each SQL Server being enumerated | SQL authentication and query execution | Always (core functionality) | +| SQL Browser | 1434/udp | UDP | SQL Server host | Named instance port resolution | Only for named instances without an explicit port. **Not proxied through SOCKS5.** | +| LDAPS | 636/tcp | TLS | Domain controller | SPN enumeration, principal/SID resolution, computer enumeration | First LDAP method attempted | +| LDAP + StartTLS | 389/tcp | TCP upgraded to TLS | Domain controller | Same as LDAPS | Fallback if LDAPS fails | +| Plain LDAP | 389/tcp | TCP (unencrypted) | Domain controller | Same as LDAPS | Final LDAP fallback | +| DNS | 53/udp | UDP | `--dns-resolver` or `--dc` | SRV records (`_ldap._tcp.`), A records, reverse DNS (PTR) | When domain resolution is needed | +| WinRM | 5985/tcp (HTTP) or 5986/tcp (HTTPS) | HTTP/HTTPS | SQL Server host | Remote PowerShell for EPA configuration | **Only `test-epa-matrix` subcommand** | +| WMI/DCOM | 135/tcp + dynamic RPC | TCP | SQL Server host | Enumerate local group members (`Win32_GroupUser`) | **Windows only.** Fails gracefully on other platforms. | + +### TDS Encryption Modes + +| Mode | Description | +|------|-------------| +| TDS 8.0 (strict) | Full TLS before any TDS traffic. Uses ALPN `tds/8.0`. | +| TLS-in-TDS | TLS negotiated inside the TDS PRELOGIN handshake. | +| Force Encryption | Server-mandated encryption after PRELOGIN exchange. | + +### LDAP Queries Issued + +| Filter | Purpose | +|--------|---------| +| `(servicePrincipalName=MSSQLSvc/*)` | Find all MSSQL SPNs in the domain | +| `(servicePrincipalName=MSSQLSvc/*)` (short + FQDN) | Look up SPNs for a specific server | +| `(&(objectCategory=computer)(objectClass=computer))` | Enumerate all domain computers (`--scan-all-computers`) | +| `(objectSid=)` | Resolve a SID to an AD principal | +| `(sAMAccountName=)` | Resolve an account name to an AD principal | +| `(&(objectClass=computer)(sAMAccountName=$))` | Resolve a computer account by name | + +All LDAP searches use subtree scope with 1000-result paging. + +## Authentication Events + +Each authentication below generates log entries on the target system. + +| Event | Target | Method | Details | Conditions | +|-------|--------|--------|---------|------------| +| SQL Server login | SQL Server | SQL auth (username/password in TDS LOGIN7) | Logged as a login event in SQL Server audit logs | When `-u`/`-p` supplied | +| SQL Server login | SQL Server | Windows auth (NTLM SSPI in TDS LOGIN7: Negotiate → Challenge → Authenticate) | Includes Channel Binding Token (CBT) when TLS is active. Logged as a login event in SQL Server audit logs. | When using domain credentials | +| LDAP bind | Domain controller | GSSAPI/SSPI (Kerberos) | Uses current user's Windows security context | Windows only, when no explicit LDAP credentials | +| LDAP bind | Domain controller | NTLM or Simple bind (UPN, DN, or DOMAIN\user) | Logged as an authentication event on the DC | When `--ldap-user`/`--ldap-password` supplied or SQL credentials reused | +| WinRM login | SQL Server host | NTLM or Basic auth | Logged as a Windows authentication event | **Only `test-epa-matrix` subcommand** | +| WMI/DCOM login | SQL Server host | Current user's Windows credentials | Logged as a DCOM authentication event | **Windows only**, during local group enumeration | + +## Subprocesses Executed + +MSSQLHound spawns local `powershell.exe` processes as fallbacks when native Go clients fail. All subprocesses run on the **operator's machine**, not on targets (except WinRM remote execution). + +| Executable | Arguments | Purpose | Conditions | +|------------|-----------|---------|------------| +| `powershell.exe` | `-NoProfile -NonInteractive -Command