From 841e75ad8d632cda801a878ca45130d6500658f4 Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Tue, 3 Sep 2019 17:38:51 -0400 Subject: [PATCH] GT-2658 - Active Directory via Kerberos authentication --- .../ghidra/server/remote/GhidraServer.java | 47 ++--- .../java/ghidra/server/remote/ServerHelp.txt | 6 +- .../server/security/AuthenticationModule.java | 10 +- ...b5ActiveDirectoryAuthenticationModule.java | 181 ++++++++++++++++++ .../jaas/jaas_external_program.example.sh | 3 +- .../Common/server/jaas/jaas_jpam.example.conf | 3 +- .../server/jaas/jaas_ldap_ad.example.conf | 2 +- .../RuntimeScripts/Common/server/server.conf | 1 + 8 files changed, 215 insertions(+), 38 deletions(-) create mode 100644 Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java index 5cd2c4ca3e..2c27eeac55 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java @@ -42,7 +42,8 @@ import org.apache.logging.log4j.Logger; import generic.jar.ResourceFile; import generic.random.SecureRandomFactory; -import ghidra.framework.*; +import ghidra.framework.Application; +import ghidra.framework.ApplicationConfiguration; import ghidra.framework.remote.*; import ghidra.net.ApplicationKeyManagerFactory; import ghidra.net.SSLContextInitializer; @@ -85,7 +86,8 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan OS_PASSWORD_LOGIN("OS Password"), PKI_LOGIN("PKI"), ALT_OS_PASSWORD_LOGIN("OS Password & Password File"), - JAAS_LOGIN("JAAS"); + JAAS_LOGIN("JAAS"), + KRB5_AD("Active Directory via Kerberos"); private String description; @@ -105,6 +107,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan case 2: return PKI_LOGIN; case 3: return ALT_OS_PASSWORD_LOGIN; case 4: return JAAS_LOGIN; + case 5: return KRB5_AD; default: return null; } //@formatter:on @@ -165,22 +168,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan requireExplicitPasswordReset = false; authModule = new PasswordFileAuthenticationModule(allowUserToSpecifyName); break; -// case ALT_OS_PASSWORD_LOGIN: -// supportLocalPasswords = true; -// case OS_PASSWORD_LOGIN: -// OperatingSystem os = OperatingSystem.CURRENT_OPERATING_SYSTEM; -// if (os == OperatingSystem.WINDOWS) { -// authModule = new NTPasswordAuthenticationModule(loginDomain, -// nameCallbackAllowed, authMode == ALT_OS_PASSWORD_LOGIN); -// } -// else if (os == UNIX) { -// authModule = new UnixPasswordAuthenticationModule(nameCallbackAllowed); -// } -// else { -// throw new IllegalArgumentException( -// "OS Password Authentication only supported for Microsoft Windows"); -// } -// break; case PKI_LOGIN: if (altSSHLoginAllowed) { log.warn("SSH authentication option ignored when PKI authentication used"); @@ -198,10 +185,22 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan case JAAS_LOGIN: authModule = new JAASAuthenticationModule("auth", allowUserToSpecifyName); break; + case KRB5_AD: + if (loginDomain == null || loginDomain.isBlank()) { + throw new IllegalArgumentException("Missing login domain value -d"); + } + authModule = new Krb5ActiveDirectoryAuthenticationModule(loginDomain, + allowUserToSpecifyName); + break; default: throw new IllegalArgumentException("Unsupported Authentication mode: " + authMode); } + if (authModule != null) { + // allow the auth modules to verify their configuration state before continuing + authModule.ensureConfig(); + } + if (altSSHLoginAllowed) { SecureRandomFactory.getSecureRandom(); // incur initialization delay up-front sshAuthModule = new SSHAuthenticationModule(allowUserToSpecifyName); @@ -356,11 +355,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan if (authModule instanceof PasswordFileAuthenticationModule) { supportPasswordChange = true; } -// else if (authModule instanceof NTPasswordAuthenticationModule) { -// supportPasswordChange = -// ((NTPasswordAuthenticationModule) authModule).usingLocalAuthentication( -// authCallbacks); -// } } else if (!mgr.getUserManager().isValidUser(username)) { FailedLoginException e = new FailedLoginException("Unknown user: " + username); @@ -550,13 +544,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan displayUsage("Invalid authentication mode: " + s); System.exit(-1); } - if (authMode == OS_PASSWORD_LOGIN || authMode == ALT_OS_PASSWORD_LOGIN) { - if (OperatingSystem.CURRENT_OPERATING_SYSTEM != OperatingSystem.WINDOWS) { - displayUsage("Authentication mode (" + authMode + - ") only supported under Microsoft Windows"); - System.exit(-1); - } - } } else if (s.startsWith("-ip")) { // setting server remote access hostname int nextArgIndex = i + 1; diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt index 8021376236..ee4805d758 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt @@ -1,5 +1,8 @@ Ghidra server startup parameters. -Command line parameters: [-ip ] [-i #.#.#.#] [-p#] [-a#] [-d] [-e] [-u] [-jaas ] [-autoProvision] [-n] +Command line parameters: + [-ip ] [-i #.#.#.#] [-p#] [-n] + [-a#] [-d] [-e] [-u] [-jaas ] [-autoProvision] + -ip : identifies the remote access IPv4 address or hostname (FQDN) which should be used by remote clients to access the server. @@ -12,6 +15,7 @@ Command line parameters: [-ip ] [-i #.#.#.#] [-p#] [-a#] [-d 0 - Private user password 2 - PKI Authentication 4 - JAAS Authentication controlled by config file pointed to by -jaas + 5 - Active Directory via Kerberos. Requires -d -anonymous : enables anonymous repository access (see svrREADME.html for details) diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AuthenticationModule.java index a4f7c008ba..8feba8ffff 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AuthenticationModule.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AuthenticationModule.java @@ -26,6 +26,10 @@ public interface AuthenticationModule { public static final String USERNAME_CALLBACK_PROMPT = "User ID"; public static final String PASSWORD_CALLBACK_PROMPT = "Password"; + default void ensureConfig() { + // default nothing + } + /** * Complete the authentication process. *

@@ -38,7 +42,7 @@ public interface AuthenticationModule { * the ones your module specified in its {@link #getAuthenticationCallbacks()} * *

- * + * *

* @param userMgr Ghidra server user manager * @param subject unauthenticated user ID (must be used if name callback not provided/allowed) @@ -56,7 +60,7 @@ public interface AuthenticationModule { /** * Allows an AuthenticationModule to deny default anonymous login steps. - *

+ *

* @return true if a separate AnonymousCallback is allowed and may be * added to the array returned by getAuthenticationCallbacks. * @see #getAuthenticationCallbacks() @@ -83,7 +87,7 @@ public interface AuthenticationModule { return null; } - // dunno if this approach is warranted. the second loop with its isInstance() may be fine. + // dunno if this approach is warranted. the second loop with its isInstance() may be fine. for (Callback cb : callbackArray) { if (callbackClass == cb.getClass()) { return callbackClass.cast(cb); diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java new file mode 100644 index 0000000000..10ff2518c9 --- /dev/null +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/Krb5ActiveDirectoryAuthenticationModule.java @@ -0,0 +1,181 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.server.security; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import javax.naming.NamingException; +import javax.naming.directory.*; +import javax.security.auth.Subject; +import javax.security.auth.callback.*; +import javax.security.auth.login.*; + +import com.sun.security.auth.module.Krb5LoginModule; + +import ghidra.framework.remote.GhidraPrincipal; +import ghidra.server.UserManager; + +/** + * A Ghidra {@link AuthenticationModule} that authenticates against an Active Directory Kerberos system + * using JAAS's {@link Krb5LoginModule}. + *

+ * This auth module needs to know the Active Directory domain name, and then from there it can bootstrap + * itself using DNS lookups to find the Kerberos server. + */ +public class Krb5ActiveDirectoryAuthenticationModule implements AuthenticationModule { + + private boolean allowUserToSpecifyName; + private String domainName; + private boolean stripDomainFromUsername = true; + + public Krb5ActiveDirectoryAuthenticationModule(String domainName, boolean allowUserToSpecifyName) { + this.domainName = domainName; + this.allowUserToSpecifyName = allowUserToSpecifyName; + } + + @Override + public void ensureConfig() { + if (domainName == null || domainName.isBlank()) { + throw new IllegalArgumentException("Missing domain name"); + } + + String loginServer = null; + try { + InetSocketAddress dc = getFirstDomainController(domainName); + loginServer = dc.getHostName(); + } + catch (NamingException e) { + // fall thru + } + if (loginServer == null) { + throw new IllegalArgumentException("No domain controller for " + domainName); + } + + System.setProperty("java.security.krb5.realm", domainName.toUpperCase()); + System.setProperty("java.security.krb5.kdc", loginServer); + } + + @Override + public String authenticate(UserManager userMgr, Subject subject, Callback[] ghidra_callbacks) + throws LoginException { + + GhidraPrincipal principal = GhidraPrincipal.getGhidraPrincipal(subject); + + AtomicReference userName = new AtomicReference<>(); + + LoginContext lc = new LoginContext("", null, (loginmodule_callbacks) -> { + // The Krb5LoginModule tends to call this callback handler multiple times. + // Once for name, and then again for password. + PasswordCallback srcPcb = AuthenticationModule.getFirstCallbackOfType( + PasswordCallback.class, ghidra_callbacks); + NameCallback srcNcb = + AuthenticationModule.getFirstCallbackOfType(NameCallback.class, ghidra_callbacks); + String tmpName = + (allowUserToSpecifyName && srcNcb != null) ? srcNcb.getName() : principal.getName(); + if (stripDomainFromUsername && tmpName.contains("\\")) { + tmpName = tmpName.replaceFirst("^.*\\\\", ""); + } + + if (tmpName == null || srcPcb == null || tmpName.isBlank()) { + throw new IOException("Missing username or password values"); + } + + NameCallback destNcb = AuthenticationModule.getFirstCallbackOfType(NameCallback.class, + loginmodule_callbacks); + PasswordCallback destPcb = AuthenticationModule.getFirstCallbackOfType( + PasswordCallback.class, loginmodule_callbacks); + + if (destNcb != null) { + destNcb.setName(tmpName); + } + + if (destPcb != null) { + destPcb.setPassword(srcPcb.getPassword()); + } + + userName.set(tmpName); + }, new JAASConfiguration("com.sun.security.auth.module.Krb5LoginModule")); + lc.login(); + + return userName.get(); + } + + @Override + public Callback[] getAuthenticationCallbacks() { + return AuthenticationModule.createSimpleNamePasswordCallbacks(allowUserToSpecifyName); + } + + @Override + public boolean anonymousCallbacksAllowed() { + return false; + } + + @Override + public boolean isNameCallbackAllowed() { + return allowUserToSpecifyName; + } + + //-------------------------------------------------------------------------------------------------- + + private static class JAASConfiguration extends Configuration { + + private AppConfigurationEntry staticConfigEntry; + private Map options = new HashMap<>(); + + public JAASConfiguration(String loginModuleClassName) { + staticConfigEntry = new AppConfigurationEntry(loginModuleClassName, + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + + return new AppConfigurationEntry[] { staticConfigEntry }; + } + + public void addOption(String name, Object value) { + options.put(name, value); + } + } + + private static final String SRV_RECORD_TYPE = "SRV"; + + private static InetSocketAddress getFirstDomainController(String domainName) + throws NamingException { + + DirContext ctx = new InitialDirContext(); + + Attributes attributes = ctx.getAttributes("dns:/_ldap._tcp.dc._msdcs." + domainName, + new String[] { SRV_RECORD_TYPE }); + if (attributes.get(SRV_RECORD_TYPE) == null) { + return null; + } + + String srvRec = attributes.get(SRV_RECORD_TYPE).get().toString(); + + String[] recParts = srvRec.split("\\s+", -1); + + int port = Integer.parseInt(recParts[2]); + String host = recParts[3]; + + return new InetSocketAddress(host, port); + } + +} diff --git a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh index e9cd8fe791..a0600edb60 100755 --- a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh +++ b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh @@ -21,8 +21,7 @@ then echo "Login failed: username has 'bad' in it: $NAME" 1>&2 exit 100 else - echo "OK" + echo "Login successful" 1>&2 fi -echo "Returning from script" 1>&2 diff --git a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_jpam.example.conf b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_jpam.example.conf index 864ff8d65d..2b0fc864fc 100644 --- a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_jpam.example.conf +++ b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_jpam.example.conf @@ -6,7 +6,8 @@ auth { net.sf.jpam.jaas.JpamLoginModule required - // the serviceName parameter controls which PAM service Ghidra will try to authenticate against + // The serviceName parameter controls which PAM service Ghidra will try to authenticate against. + // This corresponds to a file called /etc/pam.d/ serviceName="system-auth" ; }; diff --git a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_ldap_ad.example.conf b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_ldap_ad.example.conf index ca7bcf68c8..3b61ab2e5a 100644 --- a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_ldap_ad.example.conf +++ b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_ldap_ad.example.conf @@ -16,7 +16,7 @@ auth { com.sun.security.auth.module.LdapLoginModule REQUIRED userProvider="ldaps://:3269" - authIdentity="{USERNAME}@" + authIdentity="{USERNAME}@" userFilter="(sAMAccountName={USERNAME})" debug=true; }; \ No newline at end of file diff --git a/Ghidra/RuntimeScripts/Common/server/server.conf b/Ghidra/RuntimeScripts/Common/server/server.conf index ea7bcd88b7..f7886b7092 100644 --- a/Ghidra/RuntimeScripts/Common/server/server.conf +++ b/Ghidra/RuntimeScripts/Common/server/server.conf @@ -115,6 +115,7 @@ ghidra.repositories.dir=./repositories # 0 - Private user password # 2 - PKI Authentication # 4 - JAAS Authentication +# 5 - Active Directory via Kerberos. Requires -d # -anonymous : enables anonymous repository access (see svrREADME.html for details) # -ssh : enables SSH authentication for headless clients # -e : specifies default password expiration time in days (-a0 mode only, default is 1-day)