Test WinRM Connectivity on both HTTP (5985) and HTTPS (5986)

Script to test: 

 winrm_ad-test.py 

 #!/usr/bin/env python3

"""

WinRM Connectivity Tester and Active Directory Query Tool

==========================================================

This script tests connectivity to a Windows Server via WinRM on both HTTP (5985)

and HTTPS (5986) ports, and runs Active Directory queries via PowerShell.

Requirements:

 pip install pywinrm[kerberos]

 # or just:

 pip install pywinrm

Optional (for HTTPS with self-signed certs, no extra deps needed - handled below).

Usage:

 python winrm_ad_test.py --host server.domain.local --user DOMAIN\\admin --password 'Passw0rd!'

 python winrm_ad_test.py --host 10.0.0.5 --user admin@domain.local --password 'Passw0rd!' --domain domain.local

"""

import argparse

import socket

import ssl

import sys

import getpass

from contextlib import closing

try:

 import winrm

 from winrm.exceptions import (

 WinRMTransportError,

 WinRMOperationTimeoutError,

 InvalidCredentialsError,

 )

except ImportError:

 print("[!] The 'pywinrm' package is required. Install it with:")

 print(" pip install pywinrm")

 sys.exit(1)

# ---------- ANSI colors for nicer output ----------

class C:

 OK = "\033[92m"

 WARN = "\033[93m"

 FAIL = "\033[91m"

 INFO = "\033[94m"

 BOLD = "\033[1m"

 END = "\033[0m"

def banner(text: str) -> None:

 line = "=" * 70

 print(f"\n{C.BOLD}{line}\n{text}\n{line}{C.END}")

# ---------- Step 1: Raw TCP port check ----------

def check_tcp_port(host: str, port: int, timeout: float = 5.0) -> bool:

 """Confirm the TCP port is reachable before attempting WinRM handshake."""

 print(f"{C.INFO}[*] Testing TCP connectivity to {host}:{port} ...{C.END}")

 try:

 with closing(socket.create_connection((host, port), timeout=timeout)):

 print(f"{C.OK}[+] TCP port {port} is OPEN on {host}{C.END}")

 return True

 except socket.timeout:

 print(f"{C.FAIL}[-] TCP port {port} timed out (firewall? service down?){C.END}")

 except ConnectionRefusedError:

 print(f"{C.FAIL}[-] TCP port {port} refused the connection (service not listening){C.END}")

 except socket.gaierror:

 print(f"{C.FAIL}[-] Could not resolve hostname '{host}' (DNS issue){C.END}")

 except OSError as e:

 print(f"{C.FAIL}[-] TCP error on port {port}: {e}{C.END}")

 return False

# ---------- Step 2: TLS certificate check on 5986 ----------

def check_tls_cert(host: str, port: int = 5986, timeout: float = 5.0) -> None:

 """Optional: peek at the server's TLS cert presented on the HTTPS WinRM listener."""

 print(f"{C.INFO}[*] Inspecting TLS certificate on {host}:{port} ...{C.END}")

 ctx = ssl.create_default_context()

 # We don't want to fail on a self-signed cert here, we just want to see it.

 ctx.check_hostname = False

 ctx.verify_mode = ssl.CERT_NONE

 try:

 with socket.create_connection((host, port), timeout=timeout) as sock:

 with ctx.wrap_socket(sock, server_hostname=host) as ssock:

 cert = ssock.getpeercert(binary_form=False)

 # When verify_mode=CERT_NONE, getpeercert() can return {} - use binary form fallback

 if not cert:

 der = ssock.getpeercert(binary_form=True)

 print(f"{C.WARN}[!] TLS handshake OK. Server presented a certificate "

 f"({len(der)} bytes DER), details hidden (likely self-signed).{C.END}")

 else:

 subject = dict(x[0] for x in cert.get("subject", []))

 issuer = dict(x[0] for x in cert.get("issuer", []))

 print(f"{C.OK}[+] TLS handshake OK.{C.END}")

 print(f" Subject : {subject}")

 print(f" Issuer : {issuer}")

 print(f" Valid until: {cert.get('notAfter')}")

 except Exception as e:

 print(f"{C.WARN}[!] Could not retrieve TLS info: {e}{C.END}")

# ---------- Step 3: WinRM session ----------

def build_session(host: str, port: int, scheme: str, user: str, password: str,

 transport: str, verify_ssl: bool) -> winrm.Session:

 """

 Build a pywinrm Session.

 transport can be:

 - 'ntlm' (most common for AD-joined Windows servers)

 - 'kerberos' (requires kerberos client libs + valid ticket / krb5.conf)

 - 'basic' (only if AllowUnencrypted=true on server; not recommended)

 - 'ssl' 

 - 'credssp' (for double-hop scenarios)

 """

 endpoint = f"{scheme}://{host}:{port}/wsman"

 print(f"{C.INFO}[*] Opening WinRM session: {endpoint} (transport={transport}){C.END}")

 session = winrm.Session(

 endpoint,

 auth=(user, password),

 transport=transport,

 server_cert_validation="validate" if verify_ssl else "ignore",

 # operation_timeout_sec=20,

 # read_timeout_sec=30,

 )

 return session

def run_winrm_test(host: str, port: int, scheme: str, user: str, password: str,

 transport: str, verify_ssl: bool) -> winrm.Session | None:

 """Run a trivial 'whoami' / hostname command to confirm WinRM works end-to-end."""

 try:

 session = build_session(host, port, scheme, user, password, transport, verify_ssl)

 # A harmless probe command

 result = session.run_cmd("hostname")

 if result.status_code == 0:

 print(f"{C.OK}[+] WinRM authenticated successfully on {scheme.upper()}:{port}{C.END}")

 print(f" Remote hostname: {result.std_out.decode(errors='replace').strip()}")

 return session

 else:

 print(f"{C.FAIL}[-] WinRM connected but 'hostname' returned exit code "

 f"{result.status_code}{C.END}")

 print(f" stderr: {result.std_err.decode(errors='replace').strip()}")

 except InvalidCredentialsError:

 print(f"{C.FAIL}[-] Invalid credentials for {user} on {scheme.upper()}:{port}{C.END}")

 except WinRMTransportError as e:

 print(f"{C.FAIL}[-] WinRM transport error on {scheme.upper()}:{port}: {e}{C.END}")

 except WinRMOperationTimeoutError as e:

 print(f"{C.FAIL}[-] WinRM operation timed out: {e}{C.END}")

 except Exception as e:

 print(f"{C.FAIL}[-] Unexpected WinRM error on {scheme.upper()}:{port}: "

 f"{type(e).__name__}: {e}{C.END}")

 return None

# ---------- Step 4: Active Directory queries via PowerShell ----------

AD_PROBE_PS = r"""

$ErrorActionPreference = 'Stop'

try {

 Import-Module ActiveDirectory -ErrorAction Stop

 Write-Output "MODULE_OK"

} catch {

 Write-Output "MODULE_MISSING: $($_.Exception.Message)"

 exit 2

}

"""

AD_QUERIES = {

 "Domain info": r"""

 Import-Module ActiveDirectory

 Get-ADDomain | Select-Object Forest, DNSRoot, NetBIOSName, DomainMode,

 PDCEmulator, RIDMaster, InfrastructureMaster, DistinguishedName |

 Format-List

 """,

 "Forest info & FSMO roles": r"""

 Import-Module ActiveDirectory

 Get-ADForest | Select-Object Name, ForestMode, RootDomain, SchemaMaster,

 DomainNamingMaster, GlobalCatalogs, Sites |

 Format-List

 """,

 "Domain Controllers": r"""

 Import-Module ActiveDirectory

 Get-ADDomainController -Filter * |

 Select-Object Name, HostName, IPv4Address, Site, OperatingSystem,

 IsGlobalCatalog, IsReadOnly |

 Format-Table -AutoSize

 """,

 "Domain Trusts": r"""

 Import-Module ActiveDirectory

 $trusts = Get-ADTrust -Filter * -ErrorAction SilentlyContinue

 if ($trusts) {

 $trusts | Select-Object Name, Source, Target, Direction, TrustType |

 Format-Table -AutoSize

 } else {

 Write-Output "(No trusts configured)"

 }

 """,

 # ----- Generic AD object enumeration -----

 "AD object count by class": r"""

 Import-Module ActiveDirectory

 # Group every object in the directory by its objectClass to get a

 # high-level inventory of what's in AD.

 Get-ADObject -Filter * -ResultPageSize 1000 |

 Group-Object objectClass |

 Sort-Object Count -Descending |

 Select-Object @{N='ObjectClass';E={$_.Name}}, Count |

 Format-Table -AutoSize

 """,

 "Organizational Units (tree)": r"""

 Import-Module ActiveDirectory

 Get-ADOrganizationalUnit -Filter * -Properties WhenCreated |

 Sort-Object DistinguishedName |

 Select-Object Name, DistinguishedName, WhenCreated |

 Format-Table -AutoSize -Wrap

 """,

 "Containers (built-in)": r"""

 Import-Module ActiveDirectory

 # The non-OU top-level containers: Users, Computers, Builtin, etc.

 Get-ADObject -Filter 'ObjectClass -eq "container"' -SearchScope OneLevel |

 Select-Object Name, DistinguishedName |

 Format-Table -AutoSize -Wrap

 """,

 "Generic object search (first 20)": r"""

 Import-Module ActiveDirectory

 # Raw Get-ADObject query — shows you exactly what's stored in AD,

 # not filtered by user/computer/group cmdlets.

 Get-ADObject -Filter * -ResultSetSize 20 -Properties whenCreated, whenChanged |

 Select-Object Name, ObjectClass, whenCreated, whenChanged, DistinguishedName |

 Format-Table -AutoSize -Wrap

 """,

 # ----- Users -----

 "User summary (enabled vs disabled)": r"""

 Import-Module ActiveDirectory

 $all = Get-ADUser -Filter *

 $enabled = ($all | Where-Object Enabled -eq $true).Count

 $disabled = ($all | Where-Object Enabled -eq $false).Count

 [PSCustomObject]@{

 TotalUsers = $all.Count

 Enabled = $enabled

 Disabled = $disabled

 } | Format-List

 """,

 "First 10 users": r"""

 Import-Module ActiveDirectory

 Get-ADUser -Filter * -ResultSetSize 10 -Properties LastLogonDate, Enabled, PasswordLastSet |

 Select-Object SamAccountName, Name, Enabled, LastLogonDate, PasswordLastSet |

 Format-Table -AutoSize

 """,

 "Stale users (no logon in 90 days)": r"""

 Import-Module ActiveDirectory

 $cutoff = (Get-Date).AddDays(-90)

 Get-ADUser -Filter {LastLogonDate -lt $cutoff -and Enabled -eq $true} `

 -Properties LastLogonDate -ResultSetSize 10 |

 Select-Object SamAccountName, Name, LastLogonDate |

 Format-Table -AutoSize

 """,

 # ----- Computers -----

 "Computer summary by OS": r"""

 Import-Module ActiveDirectory

 Get-ADComputer -Filter * -Properties OperatingSystem |

 Group-Object OperatingSystem |

 Sort-Object Count -Descending |

 Select-Object @{N='OperatingSystem';E={if($_.Name){$_.Name}else{'(unset)'}}}, Count |

 Format-Table -AutoSize

 """,

 "First 10 computers": r"""

 Import-Module ActiveDirectory

 Get-ADComputer -Filter * -ResultSetSize 10 `

 -Properties OperatingSystem, LastLogonDate, IPv4Address |

 Select-Object Name, OperatingSystem, IPv4Address, LastLogonDate, Enabled |

 Format-Table -AutoSize

 """,

 # ----- Groups -----

 "Group summary by category/scope": r"""

 Import-Module ActiveDirectory

 Get-ADGroup -Filter * -Properties GroupCategory, GroupScope |

 Group-Object GroupCategory, GroupScope |

 Sort-Object Count -Descending |

 Select-Object @{N='Category/Scope';E={$_.Name}}, Count |

 Format-Table -AutoSize

 """,

 "Privileged groups (Domain Admins)": r"""

 Import-Module ActiveDirectory

 Get-ADGroupMember -Identity 'Domain Admins' -ErrorAction SilentlyContinue |

 Select-Object Name, SamAccountName, objectClass, distinguishedName |

 Format-Table -AutoSize -Wrap

 """,

 "Privileged groups (Enterprise Admins)": r"""

 Import-Module ActiveDirectory

 Get-ADGroupMember -Identity 'Enterprise Admins' -ErrorAction SilentlyContinue |

 Select-Object Name, SamAccountName, objectClass |

 Format-Table -AutoSize

 """,

 # ----- Group Policy & misc -----

 "Group Policy Objects": r"""

 if (Get-Module -ListAvailable -Name GroupPolicy) {

 Import-Module GroupPolicy

 Get-GPO -All |

 Select-Object DisplayName, GpoStatus, CreationTime, ModificationTime |

 Format-Table -AutoSize

 } else {

 Write-Output "(GroupPolicy module not installed on this host)"

 }

 """,

 "Service Accounts (gMSA)": r"""

 Import-Module ActiveDirectory

 $gmsa = Get-ADServiceAccount -Filter * -ErrorAction SilentlyContinue

 if ($gmsa) {

 $gmsa | Select-Object Name, SamAccountName, Enabled, ObjectClass |

 Format-Table -AutoSize

 } else {

 Write-Output "(No gMSA / managed service accounts found)"

 }

 """,

}

def run_ps(session: winrm.Session, script: str) -> tuple[int, str, str]:

 """Run a PowerShell script and return (exit_code, stdout, stderr)."""

 r = session.run_ps(script)

 return (

 r.status_code,

 r.std_out.decode("utf-8", errors="replace"),

 r.std_err.decode("utf-8", errors="replace"),

 )

def query_active_directory(session: winrm.Session, ad_filter: str | None = None) -> None:

 banner("Active Directory queries")

 # Confirm the AD module is present (only on a DC or a box with RSAT installed)

 code, out, err = run_ps(session, AD_PROBE_PS)

 if "MODULE_OK" not in out:

 print(f"{C.WARN}[!] ActiveDirectory PowerShell module is not available on this host.{C.END}")

 print(f" stdout: {out.strip()}")

 print(f" stderr: {err.strip()}")

 print(f" Tip: run this against a Domain Controller, or install RSAT-AD-PowerShell.")

 return

 print(f"{C.OK}[+] ActiveDirectory module is loaded on the remote host.{C.END}")

 # Optionally narrow the query list to labels matching a substring

 items = AD_QUERIES.items()

 if ad_filter:

 needle = ad_filter.lower()

 items = [(k, v) for k, v in AD_QUERIES.items() if needle in k.lower()]

 if not items:

 print(f"{C.WARN}[!] No AD query labels match '{ad_filter}'. "

 f"Available labels:{C.END}")

 for k in AD_QUERIES:

 print(f" - {k}")

 return

 print(f"{C.INFO}[*] Running {len(items)} queries matching '{ad_filter}'.{C.END}")

 for label, script in items:

 print(f"\n{C.BOLD}--- {label} ---{C.END}")

 code, out, err = run_ps(session, script)

 if code == 0:

 print(out.rstrip() or "(no output)")

 else:

 print(f"{C.FAIL}[-] Query failed (exit {code}){C.END}")

 if err.strip():

 print(f" stderr: {err.strip()}")

# ---------- Orchestration ----------

def test_endpoint(host: str, port: int, scheme: str, user: str, password: str,

 transport: str, verify_ssl: bool, run_ad: bool,

 ad_filter: str | None = None) -> bool:

 banner(f"Testing {scheme.upper()} on port {port}")

 if not check_tcp_port(host, port):

 return False

 if scheme == "https":

 check_tls_cert(host, port)

 session = run_winrm_test(host, port, scheme, user, password, transport, verify_ssl)

 if not session:

 return False

 if run_ad:

 query_active_directory(session, ad_filter=ad_filter)

 return True

def parse_args() -> argparse.Namespace:

 p = argparse.ArgumentParser(

 description="Test WinRM connectivity (HTTP 5985 / HTTPS 5986) and query Active Directory.",

 formatter_class=argparse.RawDescriptionHelpFormatter,

 epilog=__doc__,

 )

 p.add_argument("--host", help="Target Windows Server hostname or IP")

 p.add_argument("--user", help=r"Username (DOMAIN\user, user@domain, or local user)")

 p.add_argument("--password", help="Password (prompted if omitted)")

 p.add_argument(

 "--transport",

 default="ntlm",

 choices=["ntlm", "kerberos", "basic", "ssl", "credssp"],

 help="WinRM auth transport (default: ntlm)",

 )

 p.add_argument("--http-port", type=int, default=5985, help="HTTP WinRM port (default 5985)")

 p.add_argument("--https-port", type=int, default=5986, help="HTTPS WinRM port (default 5986)")

 p.add_argument("--only", choices=["http", "https"], help="Test only one scheme")

 p.add_argument("--verify-ssl", action="store_true",

 help="Verify the server's TLS certificate (default: ignore self-signed)")

 p.add_argument("--no-ad", action="store_true", help="Skip Active Directory queries")

 p.add_argument("--ad-filter", metavar="SUBSTRING",

 help="Run only AD queries whose label contains this substring "

 "(case-insensitive). E.g. --ad-filter object")

 p.add_argument("--list-ad-queries", action="store_true",

 help="List the available AD query labels and exit")

 return p.parse_args()

def main() -> int:

 args = parse_args()

 if args.list_ad_queries:

 print("Available AD query labels:")

 for k in AD_QUERIES:

 print(f" - {k}")

 return 0

 if not args.host or not args.user:

 print(f"{C.FAIL}[-] --host and --user are required (use --list-ad-queries "

 f"to view queries without connecting).{C.END}")

 return 2

 password = args.password or getpass.getpass(f"Password for {args.user}: ")

 results = {}

 if args.only != "https":

 results["http"] = test_endpoint(

 host=args.host,

 port=args.http_port,

 scheme="http",

 user=args.user,

 password=password,

 transport=args.transport,

 verify_ssl=False, # irrelevant for http

 run_ad=not args.no_ad,

 ad_filter=args.ad_filter,

 )

 if args.only != "http":

 results["https"] = test_endpoint(

 host=args.host,

 port=args.https_port,

 scheme="https",

 user=args.user,

 password=password,

 transport="ssl" if args.transport == "basic" else args.transport,

 verify_ssl=args.verify_ssl,

 run_ad=not args.no_ad and not results.get("http", False), # avoid running AD twice

 ad_filter=args.ad_filter,

 )

 banner("Summary")

 for scheme, ok in results.items():

 status = f"{C.OK}OK{C.END}" if ok else f"{C.FAIL}FAIL{C.END}"

 port = args.https_port if scheme == "https" else args.http_port

 print(f" {scheme.upper():5s} (port {port}): {status}")

 return 0 if any(results.values()) else 1

if __name__ == "__main__":

 try:

 sys.exit(main())

 except KeyboardInterrupt:

 print("\n[!] Interrupted by user.")

 sys.exit(130) 

 What it tests, in order: 

 

 Raw TCP check on 5985 / 5986 — distinguishes "firewall blocking" from "service down" from "DNS broken" before any auth happens. 

 TLS certificate inspection on 5986 — pulls the cert (even if self-signed) so you can see subject/issuer/expiry without failing the handshake. 

 WinRM authentication — opens a pywinrm session and runs hostname as a sanity command. 

 AD module probe — checks if the ActiveDirectory PowerShell module is loadable on the remote host (only true on a DC or a box with RSAT installed). 

 AD queries — domain info, domain controllers, first 10 users, Domain Admins members, computer count. 

 

 Install the dependency: 

 

 

 

 

 

 

 

 

 

 

 

 bash 

 

 

 pip install pywinrm 

 Run it: 

 

 

 

 

 

 

 

 

 

 

 

 bash 

 

 

 # Test both 5985 and 5986

python winrm_ad_test.py --host dc01.corp.local --user 'CORP\admin'

# Only HTTPS, skip AD queries

python winrm_ad_test.py --host 10.0.0.5 --user admin@corp.local --only https --no-ad

# Use Kerberos instead of NTLM

python winrm_ad_test.py --host dc01.corp.local --user admin@CORP.LOCAL --transport kerberos 

 A few practical notes: 

 

 WinRM over HTTP (5985) sends credentials in plaintext unless wrapped in NTLM/Kerberos message-level encryption. The script defaults to ntlm , which encrypts the payload even on port 5985 — so it works against the default Windows config. 

 HTTPS (5986) requires a listener with a cert. On a fresh Windows Server you usually need to run winrm quickconfig -transport:https or create the listener manually. The script tolerates self-signed certs by default; pass --verify-ssl if you have a real PKI. 

 The AD queries only succeed against a host where Import-Module ActiveDirectory works — that's normally a Domain Controller, or a member server with RSAT-AD-PowerShell installed. Against any other server the script gracefully reports the module is missing instead of failing. 

 For double-hop scenarios (the remote box needs to authenticate onward to a different DC), switch --transport credssp and configure CredSSP on the server.