mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 12:00:04 +02:00
GT-2658 - Active Directory via Kerberos authentication
This commit is contained in:
parent
a62730477e
commit
841e75ad8d
8 changed files with 215 additions and 38 deletions
|
@ -42,7 +42,8 @@ import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import generic.jar.ResourceFile;
|
import generic.jar.ResourceFile;
|
||||||
import generic.random.SecureRandomFactory;
|
import generic.random.SecureRandomFactory;
|
||||||
import ghidra.framework.*;
|
import ghidra.framework.Application;
|
||||||
|
import ghidra.framework.ApplicationConfiguration;
|
||||||
import ghidra.framework.remote.*;
|
import ghidra.framework.remote.*;
|
||||||
import ghidra.net.ApplicationKeyManagerFactory;
|
import ghidra.net.ApplicationKeyManagerFactory;
|
||||||
import ghidra.net.SSLContextInitializer;
|
import ghidra.net.SSLContextInitializer;
|
||||||
|
@ -85,7 +86,8 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
|
||||||
OS_PASSWORD_LOGIN("OS Password"),
|
OS_PASSWORD_LOGIN("OS Password"),
|
||||||
PKI_LOGIN("PKI"),
|
PKI_LOGIN("PKI"),
|
||||||
ALT_OS_PASSWORD_LOGIN("OS Password & Password File"),
|
ALT_OS_PASSWORD_LOGIN("OS Password & Password File"),
|
||||||
JAAS_LOGIN("JAAS");
|
JAAS_LOGIN("JAAS"),
|
||||||
|
KRB5_AD("Active Directory via Kerberos");
|
||||||
|
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@ -105,6 +107,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
|
||||||
case 2: return PKI_LOGIN;
|
case 2: return PKI_LOGIN;
|
||||||
case 3: return ALT_OS_PASSWORD_LOGIN;
|
case 3: return ALT_OS_PASSWORD_LOGIN;
|
||||||
case 4: return JAAS_LOGIN;
|
case 4: return JAAS_LOGIN;
|
||||||
|
case 5: return KRB5_AD;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
//@formatter:on
|
//@formatter:on
|
||||||
|
@ -165,22 +168,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
|
||||||
requireExplicitPasswordReset = false;
|
requireExplicitPasswordReset = false;
|
||||||
authModule = new PasswordFileAuthenticationModule(allowUserToSpecifyName);
|
authModule = new PasswordFileAuthenticationModule(allowUserToSpecifyName);
|
||||||
break;
|
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:
|
case PKI_LOGIN:
|
||||||
if (altSSHLoginAllowed) {
|
if (altSSHLoginAllowed) {
|
||||||
log.warn("SSH authentication option ignored when PKI authentication used");
|
log.warn("SSH authentication option ignored when PKI authentication used");
|
||||||
|
@ -198,10 +185,22 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
|
||||||
case JAAS_LOGIN:
|
case JAAS_LOGIN:
|
||||||
authModule = new JAASAuthenticationModule("auth", allowUserToSpecifyName);
|
authModule = new JAASAuthenticationModule("auth", allowUserToSpecifyName);
|
||||||
break;
|
break;
|
||||||
|
case KRB5_AD:
|
||||||
|
if (loginDomain == null || loginDomain.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Missing login domain value -d<domainname>");
|
||||||
|
}
|
||||||
|
authModule = new Krb5ActiveDirectoryAuthenticationModule(loginDomain,
|
||||||
|
allowUserToSpecifyName);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unsupported Authentication mode: " + authMode);
|
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) {
|
if (altSSHLoginAllowed) {
|
||||||
SecureRandomFactory.getSecureRandom(); // incur initialization delay up-front
|
SecureRandomFactory.getSecureRandom(); // incur initialization delay up-front
|
||||||
sshAuthModule = new SSHAuthenticationModule(allowUserToSpecifyName);
|
sshAuthModule = new SSHAuthenticationModule(allowUserToSpecifyName);
|
||||||
|
@ -356,11 +355,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
|
||||||
if (authModule instanceof PasswordFileAuthenticationModule) {
|
if (authModule instanceof PasswordFileAuthenticationModule) {
|
||||||
supportPasswordChange = true;
|
supportPasswordChange = true;
|
||||||
}
|
}
|
||||||
// else if (authModule instanceof NTPasswordAuthenticationModule) {
|
|
||||||
// supportPasswordChange =
|
|
||||||
// ((NTPasswordAuthenticationModule) authModule).usingLocalAuthentication(
|
|
||||||
// authCallbacks);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
else if (!mgr.getUserManager().isValidUser(username)) {
|
else if (!mgr.getUserManager().isValidUser(username)) {
|
||||||
FailedLoginException e = new FailedLoginException("Unknown user: " + username);
|
FailedLoginException e = new FailedLoginException("Unknown user: " + username);
|
||||||
|
@ -550,13 +544,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
|
||||||
displayUsage("Invalid authentication mode: " + s);
|
displayUsage("Invalid authentication mode: " + s);
|
||||||
System.exit(-1);
|
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
|
else if (s.startsWith("-ip")) { // setting server remote access hostname
|
||||||
int nextArgIndex = i + 1;
|
int nextArgIndex = i + 1;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
Ghidra server startup parameters.
|
Ghidra server startup parameters.
|
||||||
Command line parameters: [-ip <hostname>] [-i #.#.#.#] [-p#] [-a#] [-d<ntDomain>] [-e<days>] [-u] [-jaas <path_to_jaas_config_file>] [-autoProvision] [-n] <repository_path>
|
Command line parameters:
|
||||||
|
[-ip <hostname>] [-i #.#.#.#] [-p#] [-n]
|
||||||
|
[-a#] [-d<ntDomain>] [-e<days>] [-u] [-jaas <path_to_jaas_config_file>] [-autoProvision]
|
||||||
|
<repository_path>
|
||||||
|
|
||||||
-ip <hostname> : identifies the remote access IPv4 address or hostname (FQDN) which should be
|
-ip <hostname> : identifies the remote access IPv4 address or hostname (FQDN) which should be
|
||||||
used by remote clients to access the server.
|
used by remote clients to access the server.
|
||||||
|
@ -12,6 +15,7 @@ Command line parameters: [-ip <hostname>] [-i #.#.#.#] [-p#] [-a#] [-d<ntDomain>
|
||||||
0 - Private user password
|
0 - Private user password
|
||||||
2 - PKI Authentication
|
2 - PKI Authentication
|
||||||
4 - JAAS Authentication controlled by config file pointed to by -jaas
|
4 - JAAS Authentication controlled by config file pointed to by -jaas
|
||||||
|
5 - Active Directory via Kerberos. Requires -d<active_directory_domainname.tld>
|
||||||
|
|
||||||
-anonymous : enables anonymous repository access (see svrREADME.html for details)
|
-anonymous : enables anonymous repository access (see svrREADME.html for details)
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,10 @@ public interface AuthenticationModule {
|
||||||
public static final String USERNAME_CALLBACK_PROMPT = "User ID";
|
public static final String USERNAME_CALLBACK_PROMPT = "User ID";
|
||||||
public static final String PASSWORD_CALLBACK_PROMPT = "Password";
|
public static final String PASSWORD_CALLBACK_PROMPT = "Password";
|
||||||
|
|
||||||
|
default void ensureConfig() {
|
||||||
|
// default nothing
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete the authentication process.
|
* Complete the authentication process.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -38,7 +42,7 @@ public interface AuthenticationModule {
|
||||||
* the ones your module specified in its {@link #getAuthenticationCallbacks()}</li>
|
* the ones your module specified in its {@link #getAuthenticationCallbacks()}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* @param userMgr Ghidra server user manager
|
* @param userMgr Ghidra server user manager
|
||||||
* @param subject unauthenticated user ID (must be used if name callback not provided/allowed)
|
* @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.
|
* Allows an AuthenticationModule to deny default anonymous login steps.
|
||||||
* <p>
|
* <p>
|
||||||
* @return true if a separate AnonymousCallback is allowed and may be
|
* @return true if a separate AnonymousCallback is allowed and may be
|
||||||
* added to the array returned by getAuthenticationCallbacks.
|
* added to the array returned by getAuthenticationCallbacks.
|
||||||
* @see #getAuthenticationCallbacks()
|
* @see #getAuthenticationCallbacks()
|
||||||
|
@ -83,7 +87,7 @@ public interface AuthenticationModule {
|
||||||
return null;
|
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) {
|
for (Callback cb : callbackArray) {
|
||||||
if (callbackClass == cb.getClass()) {
|
if (callbackClass == cb.getClass()) {
|
||||||
return callbackClass.cast(cb);
|
return callbackClass.cast(cb);
|
||||||
|
|
|
@ -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}.
|
||||||
|
* <p>
|
||||||
|
* 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<String> 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<String, Object> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -21,8 +21,7 @@ then
|
||||||
echo "Login failed: username has 'bad' in it: $NAME" 1>&2
|
echo "Login failed: username has 'bad' in it: $NAME" 1>&2
|
||||||
exit 100
|
exit 100
|
||||||
else
|
else
|
||||||
echo "OK"
|
echo "Login successful" 1>&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Returning from script" 1>&2
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
|
|
||||||
auth {
|
auth {
|
||||||
net.sf.jpam.jaas.JpamLoginModule required
|
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>
|
||||||
serviceName="system-auth"
|
serviceName="system-auth"
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
auth {
|
auth {
|
||||||
com.sun.security.auth.module.LdapLoginModule REQUIRED
|
com.sun.security.auth.module.LdapLoginModule REQUIRED
|
||||||
userProvider="ldaps://<your_active_directory_ldap_server_hostname>:3269"
|
userProvider="ldaps://<your_active_directory_ldap_server_hostname>:3269"
|
||||||
authIdentity="{USERNAME}@<your_active_directory_domain_name>"
|
authIdentity="{USERNAME}@<your_active_directory_domain_name.tld>"
|
||||||
userFilter="(sAMAccountName={USERNAME})"
|
userFilter="(sAMAccountName={USERNAME})"
|
||||||
debug=true;
|
debug=true;
|
||||||
};
|
};
|
|
@ -115,6 +115,7 @@ ghidra.repositories.dir=./repositories
|
||||||
# 0 - Private user password
|
# 0 - Private user password
|
||||||
# 2 - PKI Authentication
|
# 2 - PKI Authentication
|
||||||
# 4 - JAAS Authentication
|
# 4 - JAAS Authentication
|
||||||
|
# 5 - Active Directory via Kerberos. Requires -d<active_directory_domainname.tld>
|
||||||
# -anonymous : enables anonymous repository access (see svrREADME.html for details)
|
# -anonymous : enables anonymous repository access (see svrREADME.html for details)
|
||||||
# -ssh : enables SSH authentication for headless clients
|
# -ssh : enables SSH authentication for headless clients
|
||||||
# -e<days> : specifies default password expiration time in days (-a0 mode only, default is 1-day)
|
# -e<days> : specifies default password expiration time in days (-a0 mode only, default is 1-day)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue