同事報案,某台未加入網域的主機原本使用 LDAP 執行帳號密碼驗證,最近被要求改用 LDAPS,ASP.NET 程式遇上奇怪的 Unknown Error (0x80005000) 錯誤。

背景知識:如同 HTTP 被要求改用 HTTPS、FTP 要改為 SFTP,伴隨著資安意識抬頭,非加密傳輸的 LDAP 協定也被盯上逼著升級成 LDAPS。依據微軟的 Windows 的 2020 LDAP 通道繫結和 LDAP 簽名要求 計劃,建議系統管理人員在 2020 年 3 月前啟用 AD 網域控制站的 LDAPS 加密通訊及 LDAP 簽署功能並完成測試,2020 年 3 月之後的 Windows 系統預設將採加密傳輸並驗證簽章。

該網站原本使用 LDAP 驗證,改用 LDAPS 後無法成功。爬文 0x80005000 錯誤疑與該主機無法驗證 AD Domain Controller 的憑證有關,LDAPS 跟 HTTPS 一樣,伺服器主機需出示由 CA 簽發的伺服器憑證向客戶端驗明正身,若客戶端無法驗證憑證真偽便拒絕連線。這個行為之前在使用 WebClient 連上 Self-Signed HTTPS 網站時常遇到(延伸閱讀:TIPS - 解決 WebClient 存取 https 網站時 SSL 憑證不符問題),簡單粗暴解法是改寫 ServerCertificateValidationCallback 傳回 true 停用憑證檢查避開問題,但這存在通訊被攔截而不自知的風險,因此,正統解法是讓客戶端信任伺服器使用的根憑證。

首先確認問題是出在伺服器憑證驗證,由於要在正式環境實測,又到了考驗野外求生技巧的時侯。我寫了以下 PowerScript:

Add-Type -AssemblyName System.DirectoryServices.Protocols
$conn = New-Object System.DirectoryServices.Protocols.LDAPConnection('dc-server.the-domain.com:636')
$options = $conn.SessionOptions
$options.ProtocolVersion = 3
#$options.VerifyServerCertificate = { $true }
$options.SecureSocketLayer = $true
$conn.AuthType = 'Basic'
$domain = Read-Host "Domain"
$username = Read-Host "UserName"
$passwd = Read-Host "Password" -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($passwd)
$passwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
$cred = New-Object System.Net.NetworkCredential($username, $passwd, $domain)
$conn.Credential = $cred
try {
    $conn.Bind();
    Write-Host "Account Validated"
}
catch {
    $_
}

用 PowerShell 建立 System.DirectoryServices.Protocols.LDAPConnection,指定啟用 SecureSocketLayer,使用 Read-Host 讀入 Domain、User Name、Password (延伸閱讀:淺談 PowerShell 中的密碼字串加密處理),建立 System.Net.NetworkCredential 建立 LDAPS 連線。注意有段 #$options.VerifyServerCertificate = { $true } 被註解起來。在問題主機上原本會出現無法連接伺服器錯誤,移除註解 VerifyServerCertificate 永遠傳回 true 可停用伺服器憑證驗證,就可登入成功。如此推斷 ASP.NET 的錯誤也是伺服器憑證驗證失敗造成。

好奇 LDAPS 伺服器的憑證長什麼樣子? VerifyServerCertificate 函式被呼叫時會傳入 LDAP 連線物件及伺服器憑證,改一下 PowerShell 程式用草船借箭法在 VerifyServerCertificate 時將憑證存成 .cer 檔。(註:{ param($p1, $p2) ... } 相當於 C# (p1, p2) => Lambda 寫法,又學到一招)

$options.VerifyServerCertificate = { 
    param($cn, $cert)
    Write-Host "VerifyServerCertificate..." | Out-Null
    $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert)
    [System.IO.File]::WriteAllBytes("D:\DC-LDAPS.cer", $cert2.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
    $true 
}

檢視 .cer 如下,是一張用網域 CA 根憑證簽發給 Domain Controller 主機的憑證:

換言之,主機加入網域會自動信任網域 CA 根憑證,故可成功驗證憑證有效。問題主機未加入網域,不認得也不信任網域 CA 根憑證,是 LDAPS 憑證驗證失敗的原因。要解決此一問題,將憑證中的 TheDomainCA 根憑證匯出安裝到問題主機,並列為為信任的根憑證即可。不熟相關操作的同學,可參考以下文章:

  1. 從 .cer 匯出根憑證的方法
  2. 憑證儲存區的選擇

實測,安裝並信任網域 CA 根憑證後,問題即告排除。

最後,偷偷說。要從非網域主機驗證 AD 帳號密碼我有個偷吃步絕招 - 在網域內 IIS 放個 txt 開 Windows 整合驗證,用 WebClient.DownloadString() 配合 NetworkCredential 傳帳號密碼下載 txt,HTTP 200 就是密碼正確,HTTP 401 就是驗證失敗,收工。

Tips of how to connect to AD domain controller from stand-alone server by trusting DC's CA root certificate.


Comments

# by 簡嬅安

感謝您的分享,很受用。

Post a comment