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 c0296aec2e..5cd2c4ca3e 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 @@ -15,6 +15,8 @@ */ package ghidra.server.remote; +import static ghidra.server.remote.GhidraServer.AUTH_MODE.*; + import java.io.*; import java.net.*; import java.rmi.NoSuchObjectException; @@ -24,6 +26,7 @@ import java.rmi.registry.Registry; import java.rmi.server.*; import java.security.cert.CertificateException; import java.util.Enumeration; +import java.util.List; import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.rmi.ssl.SslRMIServerSocketFactory; @@ -50,7 +53,9 @@ import ghidra.server.stream.BlockStreamServer; import ghidra.server.stream.RemoteBlockStreamHandle; import ghidra.util.SystemUtilities; import ghidra.util.exception.AssertException; +import ghidra.util.exception.DuplicateNameException; import resources.ResourceManager; +import utilities.util.FileUtilities; 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 USAGE_ARGS = - " [-p] [-a] [-d] [-u] [-anonymous] [-ssh] [-ip ] [-i ] [-e] [-n] "; + " [-p] [-a] [-d] [-u] [-anonymous] [-ssh] [-ip ] [-i ] [-e] [-jaas ] [-autoProvision] [-n] "; private static final String RMI_SERVER_PROPERTY = "java.rmi.server.hostname"; - private static final String[] AUTH_MODES = - { "None", "Password File", "OS Password", "PKI", "OS Password & Password File" }; + public enum AUTH_MODE { - public static final int NO_AUTH_LOGIN = -1; - public static final int PASSWORD_FILE_LOGIN = 0; - public static final int OS_PASSWORD_LOGIN = 1; - public static final int PKI_LOGIN = 2; - public static final int ALT_OS_PASSWORD_LOGIN = 3; + NO_AUTH_LOGIN("None"), + PASSWORD_FILE_LOGIN("Password File"), + OS_PASSWORD_LOGIN("OS Password"), + PKI_LOGIN("PKI"), + 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; @@ -88,31 +117,36 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan private AuthenticationModule authModule; private SSHAuthenticationModule sshAuthModule; // only supported in conjunction with password authentication modes (0 & 1) private AnonymousAuthenticationModule anonymousAuthModule; - private BlockStreamServer blockStreamServer; + private boolean autoProvisionAuthedUsers; /** * Server handle constructor. - * + * * @param rootDir * root repositories directory for server * @param authMode * authentication mode * @param loginDomain * 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 * as an alternate form of authentication * @param defaultPasswordExpirationDays number of days default password will be valid * @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 */ - GhidraServer(File rootDir, int authMode, final String loginDomain, boolean nameCallbackAllowed, - boolean altSSHLoginAllowed, int defaultPasswordExpirationDays, - boolean allowAnonymousAccess) throws IOException, CertificateException { + GhidraServer(File rootDir, AUTH_MODE authMode, String loginDomain, + boolean allowUserToSpecifyName, boolean altSSHLoginAllowed, + int defaultPasswordExpirationDays, boolean allowAnonymousAccess, + boolean autoProvisionAuthedUsers) throws IOException, CertificateException { super(ServerPortFactory.getRMISSLPort(), clientSocketFactory, serverSocketFactory); + this.autoProvisionAuthedUsers = autoProvisionAuthedUsers; + if (log == null) { // logger generally initialized by main method, however during // testing the main method may be bypassed @@ -129,7 +163,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan case PASSWORD_FILE_LOGIN: supportLocalPasswords = true; requireExplicitPasswordReset = false; - authModule = new PasswordFileAuthenticationModule(nameCallbackAllowed); + authModule = new PasswordFileAuthenticationModule(allowUserToSpecifyName); break; // case ALT_OS_PASSWORD_LOGIN: // supportLocalPasswords = true; @@ -161,13 +195,16 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan altSSHLoginAllowed = false; } break; + case JAAS_LOGIN: + authModule = new JAASAuthenticationModule("auth", allowUserToSpecifyName); + break; default: throw new IllegalArgumentException("Unsupported Authentication mode: " + authMode); } if (altSSHLoginAllowed) { SecureRandomFactory.getSecureRandom(); // incur initialization delay up-front - sshAuthModule = new SSHAuthenticationModule(nameCallbackAllowed); + sshAuthModule = new SSHAuthenticationModule(allowUserToSpecifyName); } 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()); } else if (authModule != null) { - for (Callback cb : authCallbacks) { - if (cb instanceof NameCallback) { - if (!authModule.isNameCallbackAllowed()) { - RepositoryManager.log(null, null, - "Illegal authentictaion callback: NameCallback not permitted", - username); - throw new LoginException("Illegal authentictaion callback"); - } - NameCallback nameCb = (NameCallback) cb; - String name = nameCb.getName(); - if (name == null) { - RepositoryManager.log(null, null, - "Illegal authentictaion callback: NameCallback must specify login name", - username); - throw new LoginException("Illegal authentictaion callback"); - } - username = name; - break; + NameCallback nameCb = + AuthenticationModule.getFirstCallbackOfType(NameCallback.class, authCallbacks); + if (nameCb != null) { + if (!authModule.isNameCallbackAllowed()) { + RepositoryManager.log(null, null, + "Illegal authentication callback: NameCallback not permitted", username); + throw new LoginException("Illegal authentication callback"); } + String name = nameCb.getName(); + if (name == null) { + RepositoryManager.log(null, null, + "Illegal authentication callback: NameCallback must specify login name", + username); + throw new LoginException("Illegal authentication callback"); + } + username = name; } } @@ -286,6 +320,30 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan username = authModule.authenticate(mgr.getUserManager(), user, authCallbacks); anonymousAccess = UserManager.ANONYMOUS_USERNAME.equals(username); 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", principal.getName()); } @@ -346,7 +404,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan /** * Display an optional message followed by usage syntax. - * + * * @param msg */ private static void displayUsage(String msg) { @@ -357,28 +415,14 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan } private static void displayHelp() { - InputStream in = ResourceManager.getResourceAsStream(HELP_FILE); - try { - BufferedReader br = new BufferedReader(new InputStreamReader(in)); - while (true) { - String line = br.readLine(); - if (line == null) { - break; - } - System.out.println(line); - } + + try (InputStream in = ResourceManager.getResourceAsStream(HELP_FILE)) { + List lines = FileUtilities.getLines(in); + lines.stream().forEach(s -> System.out.println(s)); } catch (IOException e) { // don't care } - finally { - try { - in.close(); - } - catch (IOException e) { - // we tried - } - } } private static final int IP_INTERFACE_RETRY_TIME_SEC = 5; @@ -444,7 +488,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan /** * Main method for starting the Ghidra server. - * + * * @param args command line arguments */ public static synchronized void main(String[] args) { @@ -463,13 +507,14 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan } int basePort = DEFAULT_PORT; - int authMode = NO_AUTH_LOGIN; + AUTH_MODE authMode = NO_AUTH_LOGIN; boolean nameCallbackAllowed = false; boolean altSSHLoginAllowed = false; boolean allowAnonymousAccess = false; String loginDomain = null; String rootPath = null; int defaultPasswordExpiration = -1; + boolean autoProvision = false; // Network name resolution disabled by default InetNameLookup.setLookupEnabled(false); @@ -490,13 +535,28 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan } } else if (s.startsWith("-a") && s.length() == 3) { // Authentication Mode + int authModeNum = Integer.MIN_VALUE; try { - authMode = Integer.parseInt(s.substring(2)); + authModeNum = Integer.parseInt(s.substring(2)); } catch (NumberFormatException e1) { displayUsage("Invalid option: " + s); 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 int nextArgIndex = i + 1; @@ -566,6 +626,27 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan 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 { if (i < (args.length - 1)) { 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) { displayUsage("Repository directory must be specified!"); System.exit(-1); @@ -686,7 +755,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan : "disabled")); // log.info(" Class server port: " + ??); log.info(" Root: " + rootPath); - log.info(" Auth: " + AUTH_MODES[authMode + 1]); + log.info(" Auth: " + authMode.getDescription()); if (authMode == PASSWORD_FILE_LOGIN && defaultPasswordExpiration >= 0) { log.info(" Default password expiration: " + (defaultPasswordExpiration == 0 ? "disabled" @@ -712,9 +781,9 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan }; clientSocketFactory = new SslRMIClientSocketFactory(); - GhidraServer svr = - new GhidraServer(serverRoot, authMode, loginDomain, nameCallbackAllowed, - altSSHLoginAllowed, defaultPasswordExpiration, allowAnonymousAccess); + GhidraServer svr = new GhidraServer(serverRoot, authMode, loginDomain, + nameCallbackAllowed, altSSHLoginAllowed, defaultPasswordExpiration, + allowAnonymousAccess, autoProvision); log.info("Registering Ghidra Server..."); 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 e1a8a603a5..8021376236 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,5 @@ Ghidra server startup parameters. -Command line parameters: [-ip ] [-i #.#.#.#] [-p#] [-a#] [-d] [-e] [-u] [-n] +Command line parameters: [-ip ] [-i #.#.#.#] [-p#] [-a#] [-d] [-e] [-u] [-jaas ] [-autoProvision] [-n] -ip : identifies the remote access IPv4 address or hostname (FQDN) which should be used by remote clients to access the server. @@ -8,9 +8,10 @@ Command line parameters: [-ip ] [-i #.#.#.#] [-p#] [-a#] [-d -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 2 - PKI Authentication + 4 - JAAS Authentication controlled by config file pointed to by -jaas -anonymous : enables anonymous repository access (see svrREADME.html for details) @@ -19,6 +20,12 @@ Command line parameters: [-ip ] [-i #.#.#.#] [-p#] [-a#] [-d -e : 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) + + -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 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 base port (default: 13100). The wrapper.log can be examined for server 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. + + \ No newline at end of file diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AnonymousAuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AnonymousAuthenticationModule.java index ea59153b6d..bceb55f631 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AnonymousAuthenticationModule.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/AnonymousAuthenticationModule.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +15,12 @@ */ package ghidra.server.security; -import ghidra.framework.remote.AnonymousCallback; - import java.util.*; import javax.security.auth.callback.Callback; +import ghidra.framework.remote.AnonymousCallback; + public class AnonymousAuthenticationModule { public Callback[] addAuthenticationCallbacks(Callback[] primaryAuthCallbacks) { @@ -34,15 +33,9 @@ public class AnonymousAuthenticationModule { } public boolean anonymousAccessRequested(Callback[] callbacks) { - if (callbacks != null) { - for (int i = 0; i < callbacks.length; i++) { - if (callbacks[i] instanceof AnonymousCallback) { - AnonymousCallback anonCb = (AnonymousCallback) callbacks[i]; - return anonCb.anonymousAccessRequested(); - } - } - } - return false; + AnonymousCallback anonCb = + AuthenticationModule.getFirstCallbackOfType(AnonymousCallback.class, callbacks); + return anonCb != null && anonCb.anonymousAccessRequested(); } } 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 a63f69048b..a4f7c008ba 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 @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +15,31 @@ */ package ghidra.server.security; -import ghidra.server.UserManager; - import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; +import javax.security.auth.callback.*; import javax.security.auth.login.LoginException; +import ghidra.server.UserManager; + 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. + *

+ * Note to AuthenticationModule implementors: + *

    + *
  • The authentication callback objects are not guaranteed to be the same + * instances as those returned by the {@link #getAuthenticationCallbacks()}.
    + * (they may have been cloned or duplicated or copied in some manner)
  • + *
  • The authentication callback array may contain callback instances other than + * 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) * @param callbacks authentication callbacks @@ -41,6 +55,8 @@ public interface AuthenticationModule { Callback[] getAuthenticationCallbacks(); /** + * 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() @@ -52,4 +68,32 @@ public interface AuthenticationModule { */ 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 getFirstCallbackOfType(Class 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; + } } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/JAASAuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/JAASAuthenticationModule.java new file mode 100644 index 0000000000..7b8c420e68 --- /dev/null +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/JAASAuthenticationModule.java @@ -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. + *

+ * JAAS is typically configured via an external file that specifies the stack of LoginModules + * per login context configuration "name". + *

+ * 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. + *

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

+ * Compatible with Apache's mod_authnz_external. + *

+ * JAAS will create a new instance of this class for each login operation. + *

+ * 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: + *

+ * 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"
+ * 	;
+ * };
+ * 
+ *

+ * 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. + *

+ * This implementation tries to follow best practices for JAAS LoginModules, even + * though Ghidra does not utilize the entire API. + *

+ * For instance, Ghidra will override JAAS LoginModule's prompt values for name and password. + *

+ * Options: + *

    + *
  • PROGRAM - path to an executable program or script.
  • + *
  • ARG_* - any number of arguments to be passed to the program.
    + * 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.
  • + *
  • TIMEOUT - number of milliseconds to wait for the external program to return results
  • + *
  • USER_PROMPT - a string to send to the user to prompt them to type their name (not used in Ghidra)
  • + *
  • PASSWORD_PROMPT - a string to send to the user to prompt them to type their password (not used in Ghidra)
  • + *
+ * + */ +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 sharedState; + private Map 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 sharedState, Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + //this.sharedState = (Map) sharedState; + this.options = (Map) 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 = 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 argKeys = options.keySet().stream().filter( + key -> key.startsWith(ARG_OPTION_NAME)).sorted().collect(Collectors.toList()); + List 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 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"); + } + } + +} diff --git a/Ghidra/Features/GhidraServer/src/test.slow/java/ghidra/server/remote/GhidraServerAWTTest.java b/Ghidra/Features/GhidraServer/src/test.slow/java/ghidra/server/remote/GhidraServerAWTTest.java index c1d886e159..d9739f4261 100644 --- a/Ghidra/Features/GhidraServer/src/test.slow/java/ghidra/server/remote/GhidraServerAWTTest.java +++ b/Ghidra/Features/GhidraServer/src/test.slow/java/ghidra/server/remote/GhidraServerAWTTest.java @@ -54,8 +54,8 @@ public class GhidraServerAWTTest extends AbstractGenericTest { // directly instantiate to avoid GhidraServer.main which may // invoke System.exit - GhidraServer server = - new GhidraServer(myTmpDir, GhidraServer.NO_AUTH_LOGIN, null, true, true, -1, true); + GhidraServer server = new GhidraServer(myTmpDir, GhidraServer.AUTH_MODE.NO_AUTH_LOGIN, + null, true, true, -1, true, false); // exercise server elements, including a repository and buffer file RepositoryManager mgr = (RepositoryManager) getInstanceField("mgr", server); @@ -66,7 +66,7 @@ public class GhidraServerAWTTest extends AbstractGenericTest { RepositoryHandle repoHandle = new RepositoryHandleImpl("test", repo); // The server will perform a timer-based file handle check once per second in test mode and - // force disposal of all disconnected handles if a getEvents has not be serviced since the + // force disposal of all disconnected handles if a getEvents has not be serviced since the // previous check eventReaderThread = new Thread(() -> { try { diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/Watchdog.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/Watchdog.java new file mode 100644 index 0000000000..d69bc43d66 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/Watchdog.java @@ -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. + *

+ * @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. + *

+ * 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); + } + +} diff --git a/Ghidra/Framework/Utility/src/main/java/utilities/util/FileUtilities.java b/Ghidra/Framework/Utility/src/main/java/utilities/util/FileUtilities.java index 8627ba4052..bfa61ef69f 100644 --- a/Ghidra/Framework/Utility/src/main/java/utilities/util/FileUtilities.java +++ b/Ghidra/Framework/Utility/src/main/java/utilities/util/FileUtilities.java @@ -25,6 +25,7 @@ import java.nio.file.FileSystem; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.*; +import java.util.function.Consumer; import generic.jar.ResourceFile; import ghidra.util.*; @@ -728,7 +729,7 @@ public final class FileUtilities { * Returns all of the lines in the given {@link InputStream} without any newline characters. *

* The input stream is closed as a side-effect. - * + * * @param is the input stream from which to read, as a side effect, it is closed * @return a {@link List} of strings representing the text lines of the file * @throws IOException if there are any issues reading the file @@ -1214,4 +1215,30 @@ public final class FileUtilities { } Desktop.getDesktop().open(file); } + + public static void asyncForEachLine(InputStream is, Consumer consumer) { + asyncForEachLine(new BufferedReader(new InputStreamReader(is)), consumer); + } + + public static void asyncForEachLine(BufferedReader reader, Consumer 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(); + } + } diff --git a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.conf b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.conf new file mode 100644 index 0000000000..7818aa5984 --- /dev/null +++ b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.conf @@ -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" + ; +}; + diff --git a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh new file mode 100755 index 0000000000..e9cd8fe791 --- /dev/null +++ b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh @@ -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 + diff --git a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_jpam.example.conf b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_jpam.example.conf new file mode 100644 index 0000000000..864ff8d65d --- /dev/null +++ b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_jpam.example.conf @@ -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" + ; +}; + diff --git a/Ghidra/RuntimeScripts/Common/server/jaas/jaas_ldap_ad.example.conf b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_ldap_ad.example.conf new file mode 100644 index 0000000000..ca7bcf68c8 --- /dev/null +++ b/Ghidra/RuntimeScripts/Common/server/jaas/jaas_ldap_ad.example.conf @@ -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://:3269" + 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 9c155dfa83..ea7bcd88b7 100644 --- a/Ghidra/RuntimeScripts/Common/server/server.conf +++ b/Ghidra/RuntimeScripts/Common/server/server.conf @@ -106,7 +106,7 @@ ghidra.repositories.dir=./repositories # Ghidra server startup parameters. # # Command line parameters: (Add command line parameters as needed and renumber each starting from .1) -# [-ip ] [-i ###.###.###.###] [-p#] [-a#] [-anonymous] [-ssh] [-d] [-e] [-u] [-n] +# [-ip ] [-i ###.###.###.###] [-p#] [-a#] [-anonymous] [-ssh] [-d] [-e] [-u] [-jaas ] [-autoProvision] [-n] # # -ip : remote access hostname or IPv4 address to be used by clients # -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 # 0 - Private user password # 2 - PKI Authentication +# 4 - JAAS Authentication # -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) # -u : enable users to be prompted for user ID (does not apply to -a2 PKI mode) +# -jaas : 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 # of reverse lookup by your DNS server) # ${ghidra.repositories.dir} : config variable (defined above) which identifies the directory diff --git a/Ghidra/RuntimeScripts/certification.manifest b/Ghidra/RuntimeScripts/certification.manifest index f9c5779579..4d90854c97 100644 --- a/Ghidra/RuntimeScripts/certification.manifest +++ b/Ghidra/RuntimeScripts/certification.manifest @@ -1,5 +1,9 @@ ##VERSION: 2.0 ##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/svrREADME.html||GHIDRA||||END| Common/support/analyzeHeadlessREADME.html||GHIDRA||||END|