Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System.DirectoryServices.Protocols.LdapConnection with SessionOptions.StartTransportLayerSecurity() sends credentials in clear text on Linux #54274

Closed
emnlz opened this issue Jun 16, 2021 · 8 comments

Comments

@emnlz
Copy link

emnlz commented Jun 16, 2021

Description

The following code snippet opens an LdapConnection, requires TLS encryption and then binds with credentials of an existing account.
The code works on both Windows and Linux, however, using a network packet sniffer shows that, when run under linux, StartTLS is ignored and credentials are sent over the network in clear text.

Moreover, the AuthType property seems to be ignored on Linux. Any value results in Basic authentication.

Code

using (var connection = new LdapConnection(new LdapDirectoryIdentifier("ldapserver")))
{
    connection.SessionOptions.StartTransportLayerSecurity(null);
    try
    {
        connection.Bind(new NetworkCredential("ldapuser", "password"));
        Console.WriteLine("ok");
    }
    finally
    {
        connection.SessionOptions.StopTransportLayerSecurity();
    }
}

The observed behavior is that encryption and authentication requirements are silently ignored on Linux.
The expected behavior is that unavailable security requirements always throw an exception and never silently fall back into a less secure scheme.

Configuration

.Net 5.0
Windows 10 and Debian 10.9 (docker mcr.microsoft.com/dotnet/runtime:5.0)

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Jun 16, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@emnlz emnlz changed the title System.DirectoryServices.Protocols.LdapConnection with SessionOptions.StartTransportLayerSecurity() sends credentials in clear text System.DirectoryServices.Protocols.LdapConnection with SessionOptions.StartTransportLayerSecurity() sends credentials in clear text on Linux Jun 16, 2021
@krwq krwq added this to the 6.0.0 milestone Jun 17, 2021
@krwq krwq removed the untriaged New issue has not been triaged by the area owner label Jun 17, 2021
@krwq
Copy link
Member

krwq commented Jun 17, 2021

[Triage] Let's try to fix this in this release

@iinuwa
Copy link
Contributor

iinuwa commented Jun 18, 2021

I think this has changed a little bit on main, but the StartTLS still doesn't work. There's are two issues:

First, LdapPal.StartTls and Interop.Ldap.ldap_start_tls have the wrong method signatures. I think that's an easy fix.

The second is more complex. OpenLDAP's ldap_start_tls needs to be called after the URI is configured for the LDAP handle. But in main, the URI isn't configured before SessionOptions.StartTransportLayerSecurity() is called by clients. This is because OpenLDAP requires the scheme to be "fully initialized." The parameters needed to determine the URI scheme are LdapDirectoryIdentifier.Connectionless, Servers and PortNumber, and SessionOptions.SecureSocketLayer. The SSL option is the problematic one, since it can't be set until after the LdapConnection is finished being constructed.

Here are a couple of options:

  • When SessionOptions.StartTransportLayerSecurity() is called, store a _startTls bool on the LdapConnection and defer calling ldap_start_tls until the Connect() phase.
  • During Init(), Initialize the LDAP handle with the ldap:// scheme (or cldap:// since LdapDirectoryIdentifier.Connectionless is known at that time). Then during Connect(), check if SecureSocketsLayer is set, and then rebuild the URI.
    • This means we'd have to build the URI list twice if SecureSocketsLayer is set.
    • If there are no other pre-bind operations that can take place using S.DS.P, then this might be an OK option.

I have a local commit that implements the first one, but it needs to be cleaned up, and I don't know how to write an automated test for it. (I've tested manually, and viewed the output in Wireshark to confirm that it works.) We can't just pass if it binds successfully with that option set, since the credentials would still fall through to the server. Maybe there's a way to restrict an LDAP server only to use StartTLS and not plain text, or to attempt to bind to a random TCP socket and detect whether the StartTLS operation was sent to it?

@iinuwa
Copy link
Contributor

iinuwa commented Jul 16, 2021

Moreover, the AuthType property seems to be ignored on Linux. Any value results in Basic authentication.

@emnlz Can you elaborate on this?

I confirmed that the StartTLS issue present in .NET 5. A reproduction program is below. I think what's happening is that if StartTransportLayerSecurity() is called, but the server certificate validation fails, the server is left thinking that the TLS session has been established, but the client bails. However, no exception is thrown when the client does this, so it continues on with the bind request in plaintext. doesn't use the TLS session when communicating.

using System;
using System.Net;
using System.Text;
using System.IO;
using System.DirectoryServices.Protocols;

var identifier = new LdapDirectoryIdentifier("ldap.local", 1389);
var cred = new NetworkCredential("cn=admin,dc=example,dc=org", "password");
using var conn = new LdapConnection(identifier, cred);
conn.SessionOptions.ProtocolVersion = 3;
var controls = new DirectoryControlCollection
{
    new DirectoryControl("7.8.9", Encoding.ASCII.GetBytes("this should show up in initial request"), false, true),
};
conn.SessionOptions.StartTransportLayerSecurity(controls);
conn.Bind();

Program throws an exception on Bind(), not StartTransportLayerSecurity():

Unhandled exception. System.DirectoryServices.Protocols.LdapException: The LDAP server returned an unknown error.
   at System.DirectoryServices.Protocols.LdapConnection.BindHelper(NetworkCredential newCredential, Boolean needSetCredential)
   at System.DirectoryServices.Protocols.LdapConnection.Bind()
   at <Program>$.<Main>$(String[] args) in /src/Program.cs:line 17

Server logs:

60f1ff04 conn=1005 fd=12 ACCEPT from IP=10.0.2.100:38460 (IP=0.0.0.0:389)
60f1ff04 conn=1005 op=0 EXT oid=1.3.6.1.4.1.1466.20037
60f1ff04 conn=1005 op=0 STARTTLS
60f1ff04 conn=1005 op=0 RESULT oid= err=0 text=
60f1ff04 conn=1005 fd=12 TLS established tls_ssf=256 ssf=256
60f1ff04 conn=1005 fd=12 closed (connection lost)

Packet capture is attached. On frame 20, the credentials are sent in cleartext after doing a TLS handshake.

If I change the TLS_CACERT setting in /etc/ldap/ldap.conf to point to the server certificate, the program binds over TLS as expected.

Tested with Docker container osixia/openldap, e.g.:

docker run --publish 1389:389 --name ldap --hostname ldap.local --detach --rm --env LDAP_TLS_VERIFY_CLIENT=never --env LDAP_ADMIN_PASSWORD=password osixia/openldap
docker exec ldap cat /container/service/slapd/assets/certs/ca.crt > ca.crt

I think the other issue that I experienced above is due to the way binding changed in .NET 6, so I'll open a separate issue for that.

@emnlz
Copy link
Author

emnlz commented Jul 19, 2021

@iinuwa If I add an explicit AuthType before Bind, whatever value I use, it produces the same packets sequence resulting in a basic authentication (i.e. cleartext password).
Under windows the authentication scheme changes according to the AuthType value (Kerberos, NTLM, Basic), but under Linux it makes no difference. The authentication always succeeds and the effective authentication scheme used is always basic (clear text).

The following code results in a STARTTLS + SASL authentication on Windows, and no TLS + basic authentication on Linux:

using (var connection = new LdapConnection(new LdapDirectoryIdentifier("ldapserver")))
{
    connection.SessionOptions.StartTransportLayerSecurity(null);
    try
    {
        connection.AuthType = AuthType.Kerberos;
        connection.Bind(new NetworkCredential("ldapuser", "password"));
        Console.WriteLine("ok");
    }
    finally
    {
        connection.SessionOptions.StopTransportLayerSecurity();
    }
}

@iinuwa
Copy link
Contributor

iinuwa commented Jul 19, 2021

Does the same AuthType issue occur without using StartTLS? If so, I think creating a separate issue would be helpful to track the two. Does setting the protocol version to 3 change anything (OpenLDAP defaults to version 2)? And have you checked out this comment? I haven't looked into the details; it looks like they're using a keytab rather than an explicit username and password like you are. I don't know if that makes a difference.

Re: StartTLS, do you think that the theory about certificate trust affecting StartTLS is valid?

@emnlz
Copy link
Author

emnlz commented Jul 20, 2021

Yes the AuthType issue seems to be unrelated to the StartTLS issue. If I remove the StartTLS, any AuthType results in a successful basic authentication on Linux.
When setting the protocol version to 3, a STARTTLS packet is emitted, but the execution then hangs on Bind and no other packet is emitted (Bind never returns).
I tried the connection with the keytab, but did not manage to make it work, probably due to DNS issues. At least I can see dns queries for the _kerberos service, meaning that a kerberos authentication is attempted.

@joperezr
Copy link
Member

This issue has now been fixed via: #60359. It was also fixed in 5.0 via #60318

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
No open projects
Development

No branches or pull requests

6 participants