mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +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.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<domainname>");
|
||||
}
|
||||
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;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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
|
||||
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
|
||||
2 - PKI Authentication
|
||||
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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
|
@ -38,7 +42,7 @@ public interface AuthenticationModule {
|
|||
* the ones your module specified in its {@link #getAuthenticationCallbacks()}</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* @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.
|
||||
* <p>
|
||||
* <p>
|
||||
* @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);
|
||||
|
|
|
@ -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
|
||||
exit 100
|
||||
else
|
||||
echo "OK"
|
||||
echo "Login successful" 1>&2
|
||||
fi
|
||||
|
||||
echo "Returning from script" 1>&2
|
||||
|
||||
|
|
|
@ -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>
|
||||
serviceName="system-auth"
|
||||
;
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
auth {
|
||||
com.sun.security.auth.module.LdapLoginModule REQUIRED
|
||||
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})"
|
||||
debug=true;
|
||||
};
|
|
@ -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<active_directory_domainname.tld>
|
||||
# -anonymous : enables anonymous repository access (see svrREADME.html for details)
|
||||
# -ssh : enables SSH authentication for headless clients
|
||||
# -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