Skip to content

Fix domain resolution for non-domain-joined runs#216

Open
ciyi wants to merge 1 commit intoSpecterOps:2.Xfrom
ciyi:fix/non-domain-joined-domain-resolution
Open

Fix domain resolution for non-domain-joined runs#216
ciyi wants to merge 1 commit intoSpecterOps:2.Xfrom
ciyi:fix/non-domain-joined-domain-resolution

Conversation

@ciyi
Copy link
Copy Markdown

@ciyi ciyi commented Apr 16, 2026

Summary

When newer versions of SharpHound are executed from a non-domain-joined machine, it often fails with: ERROR|Unable to resolve a domain to use, manually specify one or check spelling.

Fixed GetDomain to:

  • infer domain when host context domain resolution fails
  • avoid hard-failing when domain object lookup fails in GetDomainsForEnumeration
  • resolve domain SID via LDAP fallback before using Unknown SID

Reproduction

  • non-domain-joined host
  • runas /netonly context works with:
    • .\SharpHound.exe -c All --domaincontroller DC.test.local -d test.local
  • without runas, using explicit LDAP credentials previously failed during domain resolution:
    • .\SharpHound.exe -c All --domaincontroller DC.test.local --ldapusername user123 --ldappassword "Password1!" -d test.local
    • .\SharpHound.exe -c All --domaincontroller DC.test.local --ldapusername user123@test.local --ldappassword "Password1!" -d test.local

Summary by CodeRabbit

Bug Fixes

  • Improved automatic domain name detection when not explicitly provided, with clearer guidance in error messages.
  • Enhanced resilience of domain enumeration to gracefully handle partial failures and continue processing with available information rather than halting completely.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 16, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Walkthrough

Enhanced domain resolution in SharpLinks.cs by adding inference logic that parses domain information from LdapConfig when direct LDAP resolution fails, and making domain enumeration more resilient by attempting alternative SID resolution paths instead of early failure.

Changes

Cohort / File(s) Summary
Domain Resolution Enhancement
src/SharpLinks.cs
Updated Initialize method to infer domain name from LdapConfig (parsing Username for @domain or DOMAIN\user patterns, and Server for hostname domain portion) when LDAP resolution fails. Updated GetDomainsForEnumeration to continue processing with fallback SID resolution (GetDomainSidFromDomainName) when domain object retrieval fails, setting DomainSid = "Unknown" rather than faulting. Added private helper TryInferDomainName to centralize inference logic.

Sequence Diagram(s)

sequenceDiagram
    participant Client as User/Client
    participant Init as Initialize()
    participant LDAP as LDAPUtils
    participant Infer as TryInferDomainName()
    participant Config as LdapConfig

    Client->>Init: Start initialization with context
    Init->>LDAP: GetDomain(out domain)
    alt Domain resolved
        LDAP-->>Init: Success
        Init->>Client: Continue
    else Domain blank or GetDomain fails
        LDAP-->>Init: Failure/Blank
        Init->>Infer: Attempt domain inference
        Infer->>Config: Parse Username for `@domain` or DOMAIN\user
        alt Username contains domain info
            Config-->>Infer: Domain extracted
            Infer-->>Init: Success
            Init->>Client: Log info, continue
        else Try Server hostname parsing
            Infer->>Config: Parse Server for domain portion
            alt Server domain extraction succeeds
                Config-->>Infer: Domain extracted
                Infer-->>Init: Success
                Init->>Client: Log info, continue
            else All inference fails
                Infer-->>Init: Failure
                Init->>Client: Log critical error, set IsFaulted=true
            end
        end
    end
Loading
sequenceDiagram
    participant Enum as GetDomainsForEnumeration()
    participant LDAP as LDAPUtils
    participant Async as Async SID Resolver

    Enum->>LDAP: GetDomain(context.DomainName, out domain)
    alt Domain object retrieved
        LDAP-->>Enum: Success with DomainObject
        Enum->>Enum: Extract SID from domain object
        Enum->>Enum: Add to Domains collection
    else GetDomain fails
        LDAP-->>Enum: Failure/Null
        Enum->>Async: GetDomainSidFromDomainName(domain) async
        alt SID resolution succeeds
            Async-->>Enum: Return SID
            Enum->>Enum: Add domain with resolved SID
        else SID resolution fails
            Async-->>Enum: Failure
            Enum->>Enum: Log warning
            Enum->>Enum: Add domain with DomainSid = "Unknown"
        end
    end
    Enum->>Enum: Continue enumeration
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 When LDAP paths diverge, we now infer with care,
Parsing domains from config, a fallback solution fair.
No swift faults when resolution tries,
Just graceful "Unknown" where detail denies.
Resilience blooms where inference lies! 🌱

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description is partially incomplete. While it includes a clear Summary and Reproduction section, it is missing the Motivation and Context, How Has This Been Tested, Types of changes, and Checklist sections required by the template. Add missing sections: Motivation and Context (with issue reference), How Has This Been Tested (testing details), Types of changes (mark appropriate boxes), and Checklist (mark completed items).
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change—fixing domain resolution for non-domain-joined environments—which aligns with the primary objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27d7d3d4-b21a-4c57-b014-3e8bdd241ee0

📥 Commits

Reviewing files that changed from the base of the PR and between 7faa2bb and a9a9b7b.

📒 Files selected for processing (1)
  • src/SharpLinks.cs

Comment thread src/SharpLinks.cs
Comment on lines +275 to +281
if (!string.IsNullOrWhiteSpace(options.Server)) {
var server = options.Server.Trim();
var firstDot = server.IndexOf('.');
if (firstDot > 0 && firstDot < server.Length - 1) {
domainName = server.Substring(firstDot + 1);
return true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Parse options.Server before domain extraction to avoid invalid inferred domains.

Line 277 currently parses the raw server string. If Server contains a port or IP, inference can produce invalid domain values and break downstream SID lookup.

Proposed fix
             if (!string.IsNullOrWhiteSpace(options.Server)) {
-                var server = options.Server.Trim();
-                var firstDot = server.IndexOf('.');
-                if (firstDot > 0 && firstDot < server.Length - 1) {
-                    domainName = server.Substring(firstDot + 1);
+                var server = options.Server.Trim();
+                var host = server;
+
+                // Handle absolute forms like ldap://dc.test.local:389
+                if (server.Contains("://") && Uri.TryCreate(server, UriKind.Absolute, out var uri)) {
+                    host = uri.Host;
+                } else {
+                    // Handle host:port without scheme
+                    var colon = server.LastIndexOf(':');
+                    if (colon > 0 && server.IndexOf(':') == colon) {
+                        host = server.Substring(0, colon);
+                    }
+                }
+
+                // Skip IP literals; they cannot be used to infer a DNS domain safely
+                if (System.Net.IPAddress.TryParse(host, out _)) {
+                    return false;
+                }
+
+                var firstDot = host.IndexOf('.');
+                if (firstDot > 0 && firstDot < host.Length - 1) {
+                    domainName = host.Substring(firstDot + 1);
                     return true;
                 }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!string.IsNullOrWhiteSpace(options.Server)) {
var server = options.Server.Trim();
var firstDot = server.IndexOf('.');
if (firstDot > 0 && firstDot < server.Length - 1) {
domainName = server.Substring(firstDot + 1);
return true;
}
if (!string.IsNullOrWhiteSpace(options.Server)) {
var server = options.Server.Trim();
var host = server;
// Handle absolute forms like ldap://dc.test.local:389
if (server.Contains("://") && Uri.TryCreate(server, UriKind.Absolute, out var uri)) {
host = uri.Host;
} else {
// Handle host:port without scheme
var colon = server.LastIndexOf(':');
if (colon > 0 && server.IndexOf(':') == colon) {
host = server.Substring(0, colon);
}
}
// Skip IP literals; they cannot be used to infer a DNS domain safely
if (System.Net.IPAddress.TryParse(host, out _)) {
return false;
}
var firstDot = host.IndexOf('.');
if (firstDot > 0 && firstDot < host.Length - 1) {
domainName = host.Substring(firstDot + 1);
return true;
}
}

@StephenHinck
Copy link
Copy Markdown

@ciyi we'll need you to agree to the CLA to consider this PR for inclusion in the product, thank you!

@ciyi
Copy link
Copy Markdown
Author

ciyi commented Apr 16, 2026

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants