GT-2658 GhidraServer authentication via JAAS

Add JAAS auth mode -a4.
Supply some example JAAS config files.
This commit is contained in:
dev747368 2019-08-30 10:04:30 -04:00
parent 90f832bf1d
commit a62730477e
16 changed files with 915 additions and 122 deletions

View file

@ -15,6 +15,8 @@
*/ */
package ghidra.server.remote; package ghidra.server.remote;
import static ghidra.server.remote.GhidraServer.AUTH_MODE.*;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.rmi.NoSuchObjectException; import java.rmi.NoSuchObjectException;
@ -24,6 +26,7 @@ import java.rmi.registry.Registry;
import java.rmi.server.*; import java.rmi.server.*;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List;
import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory; import javax.rmi.ssl.SslRMIServerSocketFactory;
@ -50,7 +53,9 @@ import ghidra.server.stream.BlockStreamServer;
import ghidra.server.stream.RemoteBlockStreamHandle; import ghidra.server.stream.RemoteBlockStreamHandle;
import ghidra.util.SystemUtilities; import ghidra.util.SystemUtilities;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import ghidra.util.exception.DuplicateNameException;
import resources.ResourceManager; import resources.ResourceManager;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout; import utility.application.ApplicationLayout;
/** /**
@ -69,18 +74,42 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
private static String HELP_FILE = "/ghidra/server/remote/ServerHelp.txt"; private static String HELP_FILE = "/ghidra/server/remote/ServerHelp.txt";
private static String USAGE_ARGS = private static String USAGE_ARGS =
" [-p<port>] [-a<authMode>] [-d<domain>] [-u] [-anonymous] [-ssh] [-ip <hostname>] [-i <ipAddress>] [-e<expireDays>] [-n] <serverPath>"; " [-p<port>] [-a<authMode>] [-d<domain>] [-u] [-anonymous] [-ssh] [-ip <hostname>] [-i <ipAddress>] [-e<expireDays>] [-jaas <path>] [-autoProvision] [-n] <serverPath>";
private static final String RMI_SERVER_PROPERTY = "java.rmi.server.hostname"; private static final String RMI_SERVER_PROPERTY = "java.rmi.server.hostname";
private static final String[] AUTH_MODES = public enum AUTH_MODE {
{ "None", "Password File", "OS Password", "PKI", "OS Password & Password File" };
public static final int NO_AUTH_LOGIN = -1; NO_AUTH_LOGIN("None"),
public static final int PASSWORD_FILE_LOGIN = 0; PASSWORD_FILE_LOGIN("Password File"),
public static final int OS_PASSWORD_LOGIN = 1; OS_PASSWORD_LOGIN("OS Password"),
public static final int PKI_LOGIN = 2; PKI_LOGIN("PKI"),
public static final int ALT_OS_PASSWORD_LOGIN = 3; ALT_OS_PASSWORD_LOGIN("OS Password & Password File"),
JAAS_LOGIN("JAAS");
private String description;
AUTH_MODE(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public static AUTH_MODE fromIndex(int index) {
//@formatter:off
switch ( index) {
case 0: return PASSWORD_FILE_LOGIN;
case 1: return OS_PASSWORD_LOGIN;
case 2: return PKI_LOGIN;
case 3: return ALT_OS_PASSWORD_LOGIN;
case 4: return JAAS_LOGIN;
default: return null;
}
//@formatter:on
}
}
private static GhidraServer server; private static GhidraServer server;
@ -88,8 +117,8 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
private AuthenticationModule authModule; private AuthenticationModule authModule;
private SSHAuthenticationModule sshAuthModule; // only supported in conjunction with password authentication modes (0 & 1) private SSHAuthenticationModule sshAuthModule; // only supported in conjunction with password authentication modes (0 & 1)
private AnonymousAuthenticationModule anonymousAuthModule; private AnonymousAuthenticationModule anonymousAuthModule;
private BlockStreamServer blockStreamServer; private BlockStreamServer blockStreamServer;
private boolean autoProvisionAuthedUsers;
/** /**
* Server handle constructor. * Server handle constructor.
@ -100,19 +129,24 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
* authentication mode * authentication mode
* @param loginDomain * @param loginDomain
* login domain or null (used for OS_PASSWORD_LOGIN mode only) * login domain or null (used for OS_PASSWORD_LOGIN mode only)
* @param nameCallbackAllowed if true user name may be altered * @param allowUserToSpecifyName if true user name may be altered
* @param altSSHLoginAllowed if true SSH authentication will be permitted * @param altSSHLoginAllowed if true SSH authentication will be permitted
* as an alternate form of authentication * as an alternate form of authentication
* @param defaultPasswordExpirationDays number of days default password will be valid * @param defaultPasswordExpirationDays number of days default password will be valid
* @param allowAnonymousAccess allow anonymous access if true * @param allowAnonymousAccess allow anonymous access if true
* @param autoProvisionAuthedUsers flag to turn on automatically adding successfully
* authenticated users to the user manager if they don't already exist
* @throws IOException * @throws IOException
*/ */
GhidraServer(File rootDir, int authMode, final String loginDomain, boolean nameCallbackAllowed, GhidraServer(File rootDir, AUTH_MODE authMode, String loginDomain,
boolean altSSHLoginAllowed, int defaultPasswordExpirationDays, boolean allowUserToSpecifyName, boolean altSSHLoginAllowed,
boolean allowAnonymousAccess) throws IOException, CertificateException { int defaultPasswordExpirationDays, boolean allowAnonymousAccess,
boolean autoProvisionAuthedUsers) throws IOException, CertificateException {
super(ServerPortFactory.getRMISSLPort(), clientSocketFactory, serverSocketFactory); super(ServerPortFactory.getRMISSLPort(), clientSocketFactory, serverSocketFactory);
this.autoProvisionAuthedUsers = autoProvisionAuthedUsers;
if (log == null) { if (log == null) {
// logger generally initialized by main method, however during // logger generally initialized by main method, however during
// testing the main method may be bypassed // testing the main method may be bypassed
@ -129,7 +163,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
case PASSWORD_FILE_LOGIN: case PASSWORD_FILE_LOGIN:
supportLocalPasswords = true; supportLocalPasswords = true;
requireExplicitPasswordReset = false; requireExplicitPasswordReset = false;
authModule = new PasswordFileAuthenticationModule(nameCallbackAllowed); authModule = new PasswordFileAuthenticationModule(allowUserToSpecifyName);
break; break;
// case ALT_OS_PASSWORD_LOGIN: // case ALT_OS_PASSWORD_LOGIN:
// supportLocalPasswords = true; // supportLocalPasswords = true;
@ -161,13 +195,16 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
altSSHLoginAllowed = false; altSSHLoginAllowed = false;
} }
break; break;
case JAAS_LOGIN:
authModule = new JAASAuthenticationModule("auth", allowUserToSpecifyName);
break;
default: default:
throw new IllegalArgumentException("Unsupported Authentication mode: " + authMode); throw new IllegalArgumentException("Unsupported Authentication mode: " + authMode);
} }
if (altSSHLoginAllowed) { if (altSSHLoginAllowed) {
SecureRandomFactory.getSecureRandom(); // incur initialization delay up-front SecureRandomFactory.getSecureRandom(); // incur initialization delay up-front
sshAuthModule = new SSHAuthenticationModule(nameCallbackAllowed); sshAuthModule = new SSHAuthenticationModule(allowUserToSpecifyName);
} }
mgr = new RepositoryManager(rootDir, supportLocalPasswords, requireExplicitPasswordReset, mgr = new RepositoryManager(rootDir, supportLocalPasswords, requireExplicitPasswordReset,
@ -243,25 +280,22 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
RepositoryManager.log(null, null, "Anonymous access allowed", principal.getName()); RepositoryManager.log(null, null, "Anonymous access allowed", principal.getName());
} }
else if (authModule != null) { else if (authModule != null) {
for (Callback cb : authCallbacks) { NameCallback nameCb =
if (cb instanceof NameCallback) { AuthenticationModule.getFirstCallbackOfType(NameCallback.class, authCallbacks);
if (nameCb != null) {
if (!authModule.isNameCallbackAllowed()) { if (!authModule.isNameCallbackAllowed()) {
RepositoryManager.log(null, null, RepositoryManager.log(null, null,
"Illegal authentictaion callback: NameCallback not permitted", "Illegal authentication callback: NameCallback not permitted", username);
username); throw new LoginException("Illegal authentication callback");
throw new LoginException("Illegal authentictaion callback");
} }
NameCallback nameCb = (NameCallback) cb;
String name = nameCb.getName(); String name = nameCb.getName();
if (name == null) { if (name == null) {
RepositoryManager.log(null, null, RepositoryManager.log(null, null,
"Illegal authentictaion callback: NameCallback must specify login name", "Illegal authentication callback: NameCallback must specify login name",
username); username);
throw new LoginException("Illegal authentictaion callback"); throw new LoginException("Illegal authentication callback");
} }
username = name; username = name;
break;
}
} }
} }
@ -286,6 +320,30 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
username = authModule.authenticate(mgr.getUserManager(), user, authCallbacks); username = authModule.authenticate(mgr.getUserManager(), user, authCallbacks);
anonymousAccess = UserManager.ANONYMOUS_USERNAME.equals(username); anonymousAccess = UserManager.ANONYMOUS_USERNAME.equals(username);
if (!anonymousAccess) { if (!anonymousAccess) {
if (!mgr.getUserManager().isValidUser(username)) {
if (autoProvisionAuthedUsers) {
try {
mgr.getUserManager().addUser(username);
RepositoryManager.log(null, null,
"User '" + username + "' successful auto provision",
username);
}
catch (DuplicateNameException | IOException e) {
RepositoryManager.log(
null, null, "User '" + username +
"' auto provision failed. Cause: " + e.getMessage(),
username);
throw new LoginException(
"Error when trying to auto provision successfully authenticated user: " +
username);
}
}
else {
throw new LoginException(
"User successfully authenticated, but does not exist in Ghidra user list: " +
username);
}
}
RepositoryManager.log(null, null, "User '" + username + "' authenticated", RepositoryManager.log(null, null, "User '" + username + "' authenticated",
principal.getName()); principal.getName());
} }
@ -357,28 +415,14 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
} }
private static void displayHelp() { private static void displayHelp() {
InputStream in = ResourceManager.getResourceAsStream(HELP_FILE);
try { try (InputStream in = ResourceManager.getResourceAsStream(HELP_FILE)) {
BufferedReader br = new BufferedReader(new InputStreamReader(in)); List<String> lines = FileUtilities.getLines(in);
while (true) { lines.stream().forEach(s -> System.out.println(s));
String line = br.readLine();
if (line == null) {
break;
}
System.out.println(line);
}
} }
catch (IOException e) { catch (IOException e) {
// don't care // don't care
} }
finally {
try {
in.close();
}
catch (IOException e) {
// we tried
}
}
} }
private static final int IP_INTERFACE_RETRY_TIME_SEC = 5; private static final int IP_INTERFACE_RETRY_TIME_SEC = 5;
@ -463,13 +507,14 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
} }
int basePort = DEFAULT_PORT; int basePort = DEFAULT_PORT;
int authMode = NO_AUTH_LOGIN; AUTH_MODE authMode = NO_AUTH_LOGIN;
boolean nameCallbackAllowed = false; boolean nameCallbackAllowed = false;
boolean altSSHLoginAllowed = false; boolean altSSHLoginAllowed = false;
boolean allowAnonymousAccess = false; boolean allowAnonymousAccess = false;
String loginDomain = null; String loginDomain = null;
String rootPath = null; String rootPath = null;
int defaultPasswordExpiration = -1; int defaultPasswordExpiration = -1;
boolean autoProvision = false;
// Network name resolution disabled by default // Network name resolution disabled by default
InetNameLookup.setLookupEnabled(false); InetNameLookup.setLookupEnabled(false);
@ -490,13 +535,28 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
} }
} }
else if (s.startsWith("-a") && s.length() == 3) { // Authentication Mode else if (s.startsWith("-a") && s.length() == 3) { // Authentication Mode
int authModeNum = Integer.MIN_VALUE;
try { try {
authMode = Integer.parseInt(s.substring(2)); authModeNum = Integer.parseInt(s.substring(2));
} }
catch (NumberFormatException e1) { catch (NumberFormatException e1) {
displayUsage("Invalid option: " + s); displayUsage("Invalid option: " + s);
System.exit(-1); System.exit(-1);
} }
authMode = AUTH_MODE.fromIndex(authModeNum);
if (authMode == null) {
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 else if (s.startsWith("-ip")) { // setting server remote access hostname
int nextArgIndex = i + 1; int nextArgIndex = i + 1;
@ -566,6 +626,27 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
System.out.println("Default password expiration has been disbaled."); System.out.println("Default password expiration has been disbaled.");
} }
} }
else if (s.equals("-jaas")) {
int nextArgIndex = i + 1;
if (!(nextArgIndex < args.length - 1)) {
// length - 1 -> don't count mandatory repo path, which is always last arg
displayUsage("Missing -jaas config file path argument");
System.exit(-1);
}
String jaasConfigFileStr = args[nextArgIndex];
i++;
File jaasConfigFile = new File(jaasConfigFileStr);
if (!jaasConfigFile.exists() || !jaasConfigFile.isFile()) {
displayUsage("JAAS config file does not exist or is not file: " +
(new File("./").getAbsolutePath()));
System.exit(-1);
}
// NOTE: there is a leading '=' char to force this path to be the one-and-only config file
System.setProperty("java.security.auth.login.config", "=" + jaasConfigFileStr);
}
else if (s.equals("-autoProvision")) {
autoProvision = true;
}
else { else {
if (i < (args.length - 1)) { if (i < (args.length - 1)) {
displayUsage("Invalid usage!"); displayUsage("Invalid usage!");
@ -575,18 +656,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
} }
} }
if (authMode < NO_AUTH_LOGIN || authMode > ALT_OS_PASSWORD_LOGIN) {
displayUsage("Invalid authentication mode!");
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);
}
}
if (rootPath == null) { if (rootPath == null) {
displayUsage("Repository directory must be specified!"); displayUsage("Repository directory must be specified!");
System.exit(-1); System.exit(-1);
@ -686,7 +755,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
: "disabled")); : "disabled"));
// log.info(" Class server port: " + ??); // log.info(" Class server port: " + ??);
log.info(" Root: " + rootPath); log.info(" Root: " + rootPath);
log.info(" Auth: " + AUTH_MODES[authMode + 1]); log.info(" Auth: " + authMode.getDescription());
if (authMode == PASSWORD_FILE_LOGIN && defaultPasswordExpiration >= 0) { if (authMode == PASSWORD_FILE_LOGIN && defaultPasswordExpiration >= 0) {
log.info(" Default password expiration: " + log.info(" Default password expiration: " +
(defaultPasswordExpiration == 0 ? "disabled" (defaultPasswordExpiration == 0 ? "disabled"
@ -712,9 +781,9 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
}; };
clientSocketFactory = new SslRMIClientSocketFactory(); clientSocketFactory = new SslRMIClientSocketFactory();
GhidraServer svr = GhidraServer svr = new GhidraServer(serverRoot, authMode, loginDomain,
new GhidraServer(serverRoot, authMode, loginDomain, nameCallbackAllowed, nameCallbackAllowed, altSSHLoginAllowed, defaultPasswordExpiration,
altSSHLoginAllowed, defaultPasswordExpiration, allowAnonymousAccess); allowAnonymousAccess, autoProvision);
log.info("Registering Ghidra Server..."); log.info("Registering Ghidra Server...");

View file

@ -1,5 +1,5 @@
Ghidra server startup parameters. Ghidra server startup parameters.
Command line parameters: [-ip <hostname>] [-i #.#.#.#] [-p#] [-a#] [-d<ntDomain>] [-e<days>] [-u] [-n] <repository_path> Command line parameters: [-ip <hostname>] [-i #.#.#.#] [-p#] [-a#] [-d<ntDomain>] [-e<days>] [-u] [-jaas <path_to_jaas_config_file>] [-autoProvision] [-n] <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.
@ -8,9 +8,10 @@ Command line parameters: [-ip <hostname>] [-i #.#.#.#] [-p#] [-a#] [-d<ntDomain>
-p# : base TCP port to be used (default: 13100) [see Note 1] -p# : base TCP port to be used (default: 13100) [see Note 1]
-a# : an optional authentication mode where # is a value 0 or 2 -a# : an optional authentication mode where # is a value 0 or 2 or 4
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
-anonymous : enables anonymous repository access (see svrREADME.html for details) -anonymous : enables anonymous repository access (see svrREADME.html for details)
@ -20,6 +21,12 @@ Command line parameters: [-ip <hostname>] [-i #.#.#.#] [-p#] [-a#] [-d<ntDomain>
-u : enable users to be prompted for user ID (does not apply to -a2 PKI mode) -u : enable users to be prompted for user ID (does not apply to -a2 PKI mode)
-jaas /path/to/jaas_config_file : specifies config file to use for JAAS (enabled by -a4)
-autoProvision : enable the auto-creation of Ghidra users when the authenticator module
(ie. OS or other authentication method specified by JAAS) authenticates
a new unknown user.
-n : enable reverse name lookup for IP addresses when logging (requires proper configuration -n : enable reverse name lookup for IP addresses when logging (requires proper configuration
of reverse lookup by your DNS server) of reverse lookup by your DNS server)
@ -36,3 +43,11 @@ NOTES:
1. The server utilizes a total of 3 consecutive TCP ports starting with the 1. The server utilizes a total of 3 consecutive TCP ports starting with the
base port (default: 13100). The wrapper.log can be examined for server base port (default: 13100). The wrapper.log can be examined for server
console output which will indicate the startup port assignments. console output which will indicate the startup port assignments.
2. Ghidra's JAAS authentication mode uses the "auth" section of the JAAS file pointed to by the -jaas
argument.
3. Example JAAS config files are included in the /server/jaas directory of the Ghidra distro. It is the
system administrator's responsibility to create the necessary JAAS config for their system.

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,12 +15,12 @@
*/ */
package ghidra.server.security; package ghidra.server.security;
import ghidra.framework.remote.AnonymousCallback;
import java.util.*; import java.util.*;
import javax.security.auth.callback.Callback; import javax.security.auth.callback.Callback;
import ghidra.framework.remote.AnonymousCallback;
public class AnonymousAuthenticationModule { public class AnonymousAuthenticationModule {
public Callback[] addAuthenticationCallbacks(Callback[] primaryAuthCallbacks) { public Callback[] addAuthenticationCallbacks(Callback[] primaryAuthCallbacks) {
@ -34,15 +33,9 @@ public class AnonymousAuthenticationModule {
} }
public boolean anonymousAccessRequested(Callback[] callbacks) { public boolean anonymousAccessRequested(Callback[] callbacks) {
if (callbacks != null) { AnonymousCallback anonCb =
for (int i = 0; i < callbacks.length; i++) { AuthenticationModule.getFirstCallbackOfType(AnonymousCallback.class, callbacks);
if (callbacks[i] instanceof AnonymousCallback) { return anonCb != null && anonCb.anonymousAccessRequested();
AnonymousCallback anonCb = (AnonymousCallback) callbacks[i];
return anonCb.anonymousAccessRequested();
}
}
}
return false;
} }
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,16 +15,31 @@
*/ */
package ghidra.server.security; package ghidra.server.security;
import ghidra.server.UserManager;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.security.auth.callback.Callback; import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import ghidra.server.UserManager;
public interface AuthenticationModule { public interface AuthenticationModule {
public static final String USERNAME_CALLBACK_PROMPT = "User ID";
public static final String PASSWORD_CALLBACK_PROMPT = "Password";
/** /**
* Complete the authentication process * Complete the authentication process.
* <p>
* Note to AuthenticationModule implementors:
* <ul>
* <li>The authentication callback objects are not guaranteed to be the same
* instances as those returned by the {@link #getAuthenticationCallbacks()}.<br>
* (they may have been cloned or duplicated or copied in some manner)</li>
* <li>The authentication callback array may contain callback instances other than
* the ones your module specified in its {@link #getAuthenticationCallbacks()}</li>
* </ul>
* <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)
* @param callbacks authentication callbacks * @param callbacks authentication callbacks
@ -41,6 +55,8 @@ public interface AuthenticationModule {
Callback[] getAuthenticationCallbacks(); Callback[] getAuthenticationCallbacks();
/** /**
* Allows an AuthenticationModule to deny default anonymous login steps.
* <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()
@ -52,4 +68,32 @@ public interface AuthenticationModule {
*/ */
boolean isNameCallbackAllowed(); boolean isNameCallbackAllowed();
static Callback[] createSimpleNamePasswordCallbacks(boolean allowUserToSpecifyName) {
PasswordCallback passCb = new PasswordCallback(PASSWORD_CALLBACK_PROMPT + ":", false);
if (allowUserToSpecifyName) {
NameCallback nameCb = new NameCallback(USERNAME_CALLBACK_PROMPT + ":");
return new Callback[] { nameCb, passCb };
}
return new Callback[] { passCb };
}
static <T extends Callback> T getFirstCallbackOfType(Class<T> callbackClass,
Callback[] callbackArray) {
if (callbackArray == null) {
return null;
}
// 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);
}
}
for (Callback cb : callbackArray) {
if (callbackClass.isInstance(cb.getClass())) {
return callbackClass.cast(cb);
}
}
return null;
}
} }

View file

@ -0,0 +1,125 @@
/* ###
* 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.util.concurrent.atomic.AtomicReference;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import ghidra.framework.remote.GhidraPrincipal;
import ghidra.server.UserManager;
/**
* Adapter between Ghidra {@link AuthenticationModule}s and simple JAAS {@link LoginModule}s.
* <p>
* JAAS is typically configured via an external file that specifies the stack of LoginModules
* per login context configuration "name".
* <p>
* This implementation only supports JAAS LoginModules that use Name and Password callbacks,
* and ignores any customization in the name and password callbacks in favor of its own
* callbacks.
* <p>
*
*/
public class JAASAuthenticationModule implements AuthenticationModule {
private boolean allowUserToSpecifyName;
private String loginContextName;
public JAASAuthenticationModule(String loginContextName, boolean allowUserToSpecifyName) {
this.loginContextName = loginContextName;
this.allowUserToSpecifyName = allowUserToSpecifyName;
}
@Override
public String authenticate(UserManager userMgr, Subject subject, Callback[] callbacks)
throws LoginException {
GhidraPrincipal principal = GhidraPrincipal.getGhidraPrincipal(subject);
AtomicReference<String> loginName = new AtomicReference<>();
LoginContext loginCtx = new LoginContext(loginContextName, loginModuleCallbacks -> {
loginName.set(copyCallbackValues(callbacks, loginModuleCallbacks, principal));
});
// this is where the callback is triggered
loginCtx.login();
String loginNameResult = loginName.get();
return (loginNameResult != null) ? loginNameResult : principal.getName();
}
@Override
public Callback[] getAuthenticationCallbacks() {
// We don't know for sure what callbacks the JAAS LoginModule is going to throw at us
// during the login() method. Therefore, to keep things simple, we are going to limit
// the supported JAAS LoginModules to ones that only use Name and Password callbacks.
return AuthenticationModule.createSimpleNamePasswordCallbacks(allowUserToSpecifyName);
}
@Override
public boolean anonymousCallbacksAllowed() {
return false;
}
@Override
public boolean isNameCallbackAllowed() {
return allowUserToSpecifyName;
}
/**
* Copies the callback values from the callback instances in the src list to the
* corresponding instances (matched by callback class type) in the dest list, and
* then returns the user name.
*
* @param srcInstances array of callback instances to copy from
* @param destInstances array of callback instances to copy to
* @param principal the user principal (ie. default) name, used when no
* name callback is found
* @return the effective user name, either the principal or value from name callback.
* @throws IOException if missing password callback
*/
private String copyCallbackValues(Callback[] srcInstances, Callback[] destInstances,
GhidraPrincipal principal) throws IOException {
PasswordCallback srcPcb =
AuthenticationModule.getFirstCallbackOfType(PasswordCallback.class, srcInstances);
NameCallback srcNcb =
AuthenticationModule.getFirstCallbackOfType(NameCallback.class, srcInstances);
String userName = null;
NameCallback destNcb =
AuthenticationModule.getFirstCallbackOfType(NameCallback.class, destInstances);
if (destNcb != null) {
userName =
(allowUserToSpecifyName && srcNcb != null) ? srcNcb.getName() : principal.getName();
destNcb.setName(userName);
}
PasswordCallback destPcb =
AuthenticationModule.getFirstCallbackOfType(PasswordCallback.class, destInstances);
if (destPcb != null) {
if (srcPcb == null) {
throw new IOException("Missing password callback value");
}
destPcb.setPassword(srcPcb.getPassword());
}
return userName;
}
}

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +15,6 @@
*/ */
package ghidra.server.security; package ghidra.server.security;
import ghidra.framework.remote.GhidraPrincipal;
import ghidra.server.UserManager;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
@ -27,6 +23,11 @@ import javax.security.auth.callback.*;
import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import org.apache.commons.lang3.StringUtils;
import ghidra.framework.remote.GhidraPrincipal;
import ghidra.server.UserManager;
public class PasswordFileAuthenticationModule implements AuthenticationModule { public class PasswordFileAuthenticationModule implements AuthenticationModule {
private final boolean nameCallbackAllowed; private final boolean nameCallbackAllowed;
@ -43,13 +44,9 @@ public class PasswordFileAuthenticationModule implements AuthenticationModule {
/* /*
* @see ghidra.server.security.AuthenticationModule#getAuthenticationCallbacks() * @see ghidra.server.security.AuthenticationModule#getAuthenticationCallbacks()
*/ */
@Override
public Callback[] getAuthenticationCallbacks() { public Callback[] getAuthenticationCallbacks() {
PasswordCallback passCb = new PasswordCallback("Password:", false); return AuthenticationModule.createSimpleNamePasswordCallbacks(nameCallbackAllowed);
if (nameCallbackAllowed) {
NameCallback nameCb = new NameCallback("User ID:");
return new Callback[] { nameCb, passCb };
}
return new Callback[] { passCb };
} }
@Override @Override
@ -60,6 +57,7 @@ public class PasswordFileAuthenticationModule implements AuthenticationModule {
/* /*
* @see ghidra.server.security.AuthenticationModule#authenticate(ghidra.server.UserManager, javax.security.auth.Subject, javax.security.auth.callback.Callback[]) * @see ghidra.server.security.AuthenticationModule#authenticate(ghidra.server.UserManager, javax.security.auth.Subject, javax.security.auth.callback.Callback[])
*/ */
@Override
public String authenticate(UserManager userMgr, Subject subject, Callback[] callbacks) public String authenticate(UserManager userMgr, Subject subject, Callback[] callbacks)
throws LoginException { throws LoginException {
GhidraPrincipal user = GhidraPrincipal.getGhidraPrincipal(subject); GhidraPrincipal user = GhidraPrincipal.getGhidraPrincipal(subject);
@ -68,23 +66,15 @@ public class PasswordFileAuthenticationModule implements AuthenticationModule {
} }
String username = user.getName(); String username = user.getName();
NameCallback nameCb = null; NameCallback nameCb =
PasswordCallback passCb = null; AuthenticationModule.getFirstCallbackOfType(NameCallback.class, callbacks);
if (callbacks != null) { PasswordCallback passCb =
for (int i = 0; i < callbacks.length; i++) { AuthenticationModule.getFirstCallbackOfType(PasswordCallback.class, callbacks);
if (callbacks[i] instanceof NameCallback) {
nameCb = (NameCallback) callbacks[i];
}
else if (callbacks[i] instanceof PasswordCallback) {
passCb = (PasswordCallback) callbacks[i];
}
}
}
if (nameCallbackAllowed && nameCb != null) { if (nameCallbackAllowed && nameCb != null) {
username = nameCb.getName(); username = nameCb.getName();
} }
if (username == null || username.length() == 0) { if (StringUtils.isBlank(username)) {
throw new FailedLoginException("User ID must be specified"); throw new FailedLoginException("User ID must be specified");
} }
@ -92,10 +82,10 @@ public class PasswordFileAuthenticationModule implements AuthenticationModule {
throw new FailedLoginException("Password callback required"); throw new FailedLoginException("Password callback required");
} }
char[] pass = null; char[] pass = passCb.getPassword();
try {
pass = passCb.getPassword();
passCb.clearPassword(); passCb.clearPassword();
try {
userMgr.authenticateUser(username, pass); userMgr.authenticateUser(username, pass);
} }
catch (IOException e) { catch (IOException e) {

View file

@ -0,0 +1,337 @@
/* ###
* 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.loginmodule;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import com.sun.security.auth.UserPrincipal;
import ghidra.server.RepositoryManager;
import ghidra.util.DateUtils;
import ghidra.util.timer.Watchdog;
import utilities.util.FileUtilities;
/**
* A JAAS {@link LoginModule} that executes an external program that decides if the username
* and password are authorized.
* <p>
* Compatible with Apache's mod_authnz_external.
* <p>
* JAAS will create a new instance of this class for each login operation.
* <p>
* The options for this module (the path to the external program, timeout values, etc)
* are supplied to the {@link #initialize(Subject, CallbackHandler, Map, Map)}
* by JAAS and are typically read from a config file that looks like:
* <pre>
* auth {
* ghidra.server.security.loginmodule.ExternalProgramLoginModule required
* PROGRAM="jaas_external_program.example.sh"
* ARG_00="arg1" ARG_01="test arg2"
* TIMEOUT="1000"
* USER_PROMPT="Enter username"
* PASSWORD_PROMPT="Enter password"
* ;
* };
* </pre>
* <p>
* The external program is fed the username\n and password\n on its STDIN (ie. two text lines).
* The external authenticator needs to exit with 0 (zero) error level
* if the authentication was successful, or a non-zero error level if not successful.
* <p>
* This implementation tries to follow best practices for JAAS LoginModules, even
* though Ghidra does not utilize the entire API.
* <p>
* For instance, Ghidra will override JAAS LoginModule's prompt values for name and password.
* <p>
* Options:
* <ul>
* <li>PROGRAM - path to an executable program or script.</li>
* <li>ARG_* - any number of arguments to be passed to the program.<br>
* Example: ARG_00="foo" ARG_01="bar". Arguments are ordered according to their natural
* sorting order, so it is advisable to keep the suffixes to the same length.</li>
* <li>TIMEOUT - number of milliseconds to wait for the external program to return results</li>
* <li>USER_PROMPT - a string to send to the user to prompt them to type their name (not used in Ghidra)</li>
* <li>PASSWORD_PROMPT - a string to send to the user to prompt them to type their password (not used in Ghidra)</li>
* </ul>
*
*/
public class ExternalProgramLoginModule implements LoginModule {
// private static final String USERNAME_KEY = "javax.security.auth.login.name";
// private static final String PASSWORD_KEY = "javax.security.auth.login.password";
private static final String USER_PROMPT_OPTION_NAME = "USER_PROMPT";
private static final String PASSWORD_PROMPT_OPTION_NAME = "PASSWORD_PROMPT";
private static final String TIMEOUT_OPTION_NAME = "TIMEOUT";
private static final String PROGRAM_OPTION_NAME = "PROGRAM";
private static final String ARG_OPTION_NAME = "ARG_";
private static final long DEFAULT_TIMEOUT_MS = DateUtils.MS_PER_SEC * 10;
private Subject subject;
private CallbackHandler callbackHandler;
//private Map<String, Object> sharedState;
private Map<String, Object> options;
//private boolean useSharedState;
//private boolean clearSharedCreds;
private UserPrincipal user;
private String username;
private char[] password;
private String[] cmdArray;
private String extProgramName;
private boolean success;
private boolean committed;
private long timeout_ms = DEFAULT_TIMEOUT_MS;
@SuppressWarnings("unchecked")
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler,
Map<String, ?> sharedState, Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
//this.sharedState = (Map<String, Object>) sharedState;
this.options = (Map<String, Object>) options;
}
@Override
public boolean login() throws LoginException {
readOptions();
getNameAndPassword();
callExternalProgram();
success = true;
user = new UserPrincipal(username);
return true;
}
@Override
public boolean commit() throws LoginException {
if (!success) {
return false;
}
if (!subject.isReadOnly()) {
if (!user.implies(subject)) {
subject.getPrincipals().add(user);
}
}
committed = true;
return true;
}
@Override
public boolean abort() throws LoginException {
if (!success) {
return false;
}
if (!committed) {
success = false;
cleanup();
}
else {
logout();
}
return true;
}
@Override
public boolean logout() throws LoginException {
if (subject.isReadOnly()) {
cleanup();
throw new LoginException("Subject is read-only");
}
subject.getPrincipals().remove(user);
cleanup();
success = false;
committed = false;
return true;
}
private void cleanup() {
user = null;
username = null;
if (password != null) {
Arrays.fill(password, '\0');
password = null;
}
/* not impl yet
if (clearSharedCreds) {
sharedState.remove(USERNAME_KEY);
sharedState.remove(PASSWORD_KEY);
} */
}
private void readOptions() throws LoginException {
String timeoutStr = (String) options.get(TIMEOUT_OPTION_NAME);
if (timeoutStr != null) {
try {
timeout_ms = Long.parseLong(timeoutStr);
}
catch (NumberFormatException e) {
// ignore, leave timeout at default 10sec
}
}
readExtProgOptions();
}
private void callExternalProgram() throws LoginException {
AtomicReference<Process> process = new AtomicReference<>();
try (Watchdog watchdog = new Watchdog(timeout_ms, () -> {
Process local_p = process.get();
if (local_p != null) {
local_p.destroyForcibly();
}
})) {
watchdog.arm();
Process p = Runtime.getRuntime().exec(cmdArray);
process.set(p);
FileUtilities.asyncForEachLine(p.getInputStream(), (stdOutStr) -> {
RepositoryManager.log(null, null, extProgramName + " STDOUT: " + stdOutStr, null);
});
FileUtilities.asyncForEachLine(p.getErrorStream(), (errStr) -> {
RepositoryManager.log(null, null, extProgramName + " STDERR: " + errStr, null);
});
PrintWriter outputWriter = new PrintWriter(p.getOutputStream());
outputWriter.write(username);
outputWriter.write("\n");
outputWriter.write(password);
outputWriter.write("\n");
outputWriter.flush();
int exitValue = p.waitFor();
if (exitValue != 0) {
throw new FailedLoginException(
"Login failed: external command exited with error " + exitValue);
}
}
catch (IOException | InterruptedException e) {
RepositoryManager.log(null, null,
"Exception when executing " + extProgramName + ":" + e.getMessage(), null);
throw new LoginException("Error executing external program");
}
finally {
Process p = process.get();
if (p != null && p.isAlive()) {
if (p.isAlive()) {
try {
p.waitFor(timeout_ms, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
// ignore
}
finally {
p.destroyForcibly();
}
}
}
}
}
private void readExtProgOptions() throws LoginException {
String externalProgram = (String) options.get(PROGRAM_OPTION_NAME);
if (externalProgram == null || externalProgram.isBlank()) {
throw new LoginException(
"Missing " + PROGRAM_OPTION_NAME + "=path_to_external_program in options");
}
File extProFile = new File(externalProgram).getAbsoluteFile();
if (!extProFile.exists()) {
throw new LoginException(
"Bad " + PROGRAM_OPTION_NAME + "=path_to_external_program in options");
}
extProgramName = extProFile.getName();
List<String> argKeys = options.keySet().stream().filter(
key -> key.startsWith(ARG_OPTION_NAME)).sorted().collect(Collectors.toList());
List<String> cmdArrayValues = new ArrayList<>();
cmdArrayValues.add(externalProgram.toString());
for (String argKey : argKeys) {
String val = options.get(argKey).toString();
cmdArrayValues.add(val);
}
cmdArray = cmdArrayValues.toArray(new String[cmdArrayValues.size()]);
}
private void getNameAndPassword() throws LoginException {
String userPrompt = options.getOrDefault(USER_PROMPT_OPTION_NAME, "User name").toString();
String passPrompt =
options.getOrDefault(PASSWORD_PROMPT_OPTION_NAME, "Password").toString();
List<Callback> callbacks = new ArrayList<>();
NameCallback ncb = null;
PasswordCallback pcb = null;
/* not impl yet
if (useSharedState) {
username = (String) sharedState.get(USERNAME_KEY);
password = (char[]) sharedState.get(PASSWORD_KEY);
if (password != null) {
password = password.clone();
}
} */
if (username == null) {
ncb = new NameCallback(userPrompt);
callbacks.add(ncb);
}
if (password == null) {
pcb = new PasswordCallback(passPrompt, false);
callbacks.add(pcb);
}
if (!callbacks.isEmpty()) {
try {
callbackHandler.handle(callbacks.toArray(new Callback[callbacks.size()]));
if (ncb != null) {
username = ncb.getName();
}
if (pcb != null) {
password = pcb.getPassword();
pcb.clearPassword();
}
if (username == null || password == null) {
throw new LoginException("Failed to get username or password");
}
}
catch (IOException | UnsupportedCallbackException e) {
throw new LoginException("Error during callback: " + e.getMessage());
}
}
validateUsernameAndPasswordFormat();
}
private void validateUsernameAndPasswordFormat() throws LoginException {
if (username.contains("\n") || username.contains("\0")) {
throw new LoginException("Bad characters in username");
}
String tmpPass = String.valueOf(password);
if (tmpPass.contains("\n") || tmpPass.contains("\0")) {
throw new LoginException("Bad characters in password");
}
}
}

View file

@ -54,8 +54,8 @@ public class GhidraServerAWTTest extends AbstractGenericTest {
// directly instantiate to avoid GhidraServer.main which may // directly instantiate to avoid GhidraServer.main which may
// invoke System.exit // invoke System.exit
GhidraServer server = GhidraServer server = new GhidraServer(myTmpDir, GhidraServer.AUTH_MODE.NO_AUTH_LOGIN,
new GhidraServer(myTmpDir, GhidraServer.NO_AUTH_LOGIN, null, true, true, -1, true); null, true, true, -1, true, false);
// exercise server elements, including a repository and buffer file // exercise server elements, including a repository and buffer file
RepositoryManager mgr = (RepositoryManager) getInstanceField("mgr", server); RepositoryManager mgr = (RepositoryManager) getInstanceField("mgr", server);

View file

@ -0,0 +1,109 @@
/* ###
* 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.util.timer;
import java.io.Closeable;
import java.util.concurrent.atomic.AtomicLong;
import ghidra.util.Msg;
/**
* A reusable watchdog that will execute a callback if the watchdog is not disarmed before
* it expires.
*
*/
public class Watchdog implements Closeable {
private long defaultWatchdogTimeoutMS;
private AtomicLong watchdogExpiresAt = new AtomicLong();
private Runnable timeoutMethod;
private GTimerMonitor watchdogTimer;
/**
* Creates a watchdog (initially disarmed) that will poll for expiration every
* defaultTimeoutMS milliseconds, calling {@code timeoutMethod} when triggered.
* <p>
* @param defaultTimeoutMS number of milliseconds that the watchdog will wait after
* being armed before calling the timeout method.
* @param timeoutMethod {@link Runnable} functional callback.
*/
public Watchdog(long defaultTimeoutMS, Runnable timeoutMethod) {
this.defaultWatchdogTimeoutMS = defaultTimeoutMS;
this.timeoutMethod = timeoutMethod;
this.watchdogTimer = GTimer.scheduleRepeatingRunnable(defaultTimeoutMS, defaultTimeoutMS,
this::watchdogWorker);
}
@Override
public void finalize() {
if (watchdogTimer != null) {
close();
Msg.warn(this, "Unclosed Watchdog");
}
}
/**
* Releases the background timer that this watchdog uses.
*/
@Override
public void close() {
if (watchdogTimer != null) {
watchdogTimer.cancel();
}
watchdogTimer = null;
timeoutMethod = null;
}
/**
* Called from a timer, checks to see if the watchdog is armed, and if it has expired.
* <p>
* Disarms itself before calling the timeoutMethod if the timeout period expired.
*/
private void watchdogWorker() {
long expiresAt = watchdogExpiresAt.get();
if (expiresAt > 0) {
long now = System.currentTimeMillis();
if (now > expiresAt) {
setEnabled(false);
timeoutMethod.run();
}
}
}
private void setEnabled(boolean b) {
watchdogExpiresAt.set(b ? System.currentTimeMillis() + defaultWatchdogTimeoutMS : -1);
}
public boolean isEnabled() {
return watchdogExpiresAt.get() > 0;
}
/**
* Enables this watchdog so that at {@link #defaultWatchdogTimeoutMS} milliseconds in the
* future the {@link #timeoutMethod} will be called.
*/
public void arm() {
setEnabled(true);
}
/**
* Disables this watchdog.
*/
public void disarm() {
setEnabled(false);
}
}

View file

@ -25,6 +25,7 @@ import java.nio.file.FileSystem;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.*; import java.util.*;
import java.util.function.Consumer;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.util.*; import ghidra.util.*;
@ -1214,4 +1215,30 @@ public final class FileUtilities {
} }
Desktop.getDesktop().open(file); Desktop.getDesktop().open(file);
} }
public static void asyncForEachLine(InputStream is, Consumer<String> consumer) {
asyncForEachLine(new BufferedReader(new InputStreamReader(is)), consumer);
}
public static void asyncForEachLine(BufferedReader reader, Consumer<String> consumer) {
new Thread(() -> {
try {
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
consumer.accept(line);
}
}
catch (IOException ioe) {
// ignore io errors while reading because thats normal when hitting EOF
}
catch (Exception e) {
Msg.error(FileUtilities.class, "Exception while reading", e);
}
}, "Threaded Stream Reader Thread").start();
}
} }

View file

@ -0,0 +1,12 @@
// Example JAAS config file for Ghidra server when operating in -a4 authmode.
// Ghidra only uses the "auth" section from the JAAS configuration.
// You may need to adjust the PROGRAM="" to include the full path to the example script
auth {
ghidra.server.security.loginmodule.ExternalProgramLoginModule required
PROGRAM="server/jaas/jaas_external_program.example.sh"
TIMEOUT="1000"
ARG_00="arg1" ARG_01="test arg2"
;
};

View file

@ -0,0 +1,28 @@
#!/bin/bash
# This is a trivial example to show how the Ghidra ExternalProgramLoginModule
# communicates with the external authenticator.
#
# The username and password will be supplied on STDIN separated by a newline.
# No other data will be sent on STDIN.
#
# The external authenticator (this script) needs to exit with 0 (zero) error level
# if the authentication was successful, or a non-zero error level if not successful.
#
echo "Starting example JAAS external auth script" 1>&2
read NAME
read PASSWORD
if [[ ${NAME} =~ "bad" ]]
then
echo "Login failed: username has 'bad' in it: $NAME" 1>&2
exit 100
else
echo "OK"
fi
echo "Returning from script" 1>&2

View file

@ -0,0 +1,13 @@
// Example JAAS config file to use the local Linux PAM system when operating in -a4 authmode.
// JPAM is not included in the Ghidra distro.
// Additionally:
// the libjpam.so native library needs to be copied to your ${JAVA_HOME}/lib directory.
// the JPAM-x.y.jar java library needs to be inserted into the GhidraServer's classpath.
auth {
net.sf.jpam.jaas.JpamLoginModule required
// the serviceName parameter controls which PAM service Ghidra will try to authenticate against
serviceName="system-auth"
;
};

View file

@ -0,0 +1,22 @@
// Example JAAS config file to use an Active Directory LDAP server to authenticate users when operating in -a4 authmode.
//
// The special string "{USERNAME}" in the authIdentity and userFilter parameters is replaced with the Ghidra user's name
// at runtime by the LdapLoginModule, and should not be modified.
//
// The ldap DNS hostname for your Active Directory server needs to be fixed-up in the userProvider parameter,
// and the domain name portion of your user's identity (ie. user@domain.tld) needs to be fixed up in the
// authIdentity parameter.
//
// In this mode, the Ghidra Server will bind to the LDAP server using the Ghidra user's name and password. It will
// then query for that same user (sAMAccountName={USERNAME}) to confirm that user's DN.
//
// See https://docs.oracle.com/javase/8/docs/jre/api/security/jaas/spec/com/sun/security/auth/module/LdapLoginModule.html
// for more information about the LdapLoginModule and its configuration.
//
auth {
com.sun.security.auth.module.LdapLoginModule REQUIRED
userProvider="ldaps://<your_active_directory_ldap_server_hostname>:3269"
authIdentity="{USERNAME}@<your_active_directory_domain_name>"
userFilter="(sAMAccountName={USERNAME})"
debug=true;
};

View file

@ -106,7 +106,7 @@ ghidra.repositories.dir=./repositories
# Ghidra server startup parameters. # Ghidra server startup parameters.
# #
# Command line parameters: (Add command line parameters as needed and renumber each starting from .1) # Command line parameters: (Add command line parameters as needed and renumber each starting from .1)
# [-ip <hostname>] [-i ###.###.###.###] [-p#] [-a#] [-anonymous] [-ssh] [-d<ntDomain>] [-e<days>] [-u] [-n] <repositories_path> # [-ip <hostname>] [-i ###.###.###.###] [-p#] [-a#] [-anonymous] [-ssh] [-d<ntDomain>] [-e<days>] [-u] [-jaas <config_file>] [-autoProvision] [-n] <repositories_path>
# #
# -ip <hostname> : remote access hostname or IPv4 address to be used by clients # -ip <hostname> : remote access hostname or IPv4 address to be used by clients
# -i #.#.#.# : interface IPv4 address to accept connections on (default all interfaces) # -i #.#.#.# : interface IPv4 address to accept connections on (default all interfaces)
@ -114,10 +114,15 @@ ghidra.repositories.dir=./repositories
# -a# : an optional authentication mode where # is a value 0 or 2 # -a# : an optional authentication mode where # is a value 0 or 2
# 0 - Private user password # 0 - Private user password
# 2 - PKI Authentication # 2 - PKI Authentication
# 4 - JAAS Authentication
# -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)
# -u : enable users to be prompted for user ID (does not apply to -a2 PKI mode) # -u : enable users to be prompted for user ID (does not apply to -a2 PKI mode)
# -jaas <path_to_config_file> : specifies JAAS config file.
# -autoProvision : enable the auto-creation of Ghidra users when the authenticator module
# (ie. OS or other authentication method specified by JAAS) authenticates
# a new unknown user.
# -n : enable reverse name lookup for IP addresses when logging (requires proper configuration # -n : enable reverse name lookup for IP addresses when logging (requires proper configuration
# of reverse lookup by your DNS server) # of reverse lookup by your DNS server)
# ${ghidra.repositories.dir} : config variable (defined above) which identifies the directory # ${ghidra.repositories.dir} : config variable (defined above) which identifies the directory

View file

@ -1,5 +1,9 @@
##VERSION: 2.0 ##VERSION: 2.0
##MODULE IP: Copyright Distribution Permitted ##MODULE IP: Copyright Distribution Permitted
Common/server/jaas/jaas_external_program.example.conf||GHIDRA||||END|
Common/server/jaas/jaas_external_program.example.sh||GHIDRA||||END|
Common/server/jaas/jaas_jpam.example.conf||GHIDRA||||END|
Common/server/jaas/jaas_ldap_ad.example.conf||GHIDRA||||END|
Common/server/server.conf||GHIDRA||||END| Common/server/server.conf||GHIDRA||||END|
Common/server/svrREADME.html||GHIDRA||||END| Common/server/svrREADME.html||GHIDRA||||END|
Common/support/analyzeHeadlessREADME.html||GHIDRA||||END| Common/support/analyzeHeadlessREADME.html||GHIDRA||||END|