mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
GT-2658 GhidraServer authentication via JAAS
Add JAAS auth mode -a4. Supply some example JAAS config files.
This commit is contained in:
parent
90f832bf1d
commit
a62730477e
16 changed files with 915 additions and 122 deletions
|
@ -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<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[] 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,8 +117,8 @@ 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.
|
||||
|
@ -100,19 +129,24 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan
|
|||
* 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) {
|
||||
NameCallback nameCb =
|
||||
AuthenticationModule.getFirstCallbackOfType(NameCallback.class, authCallbacks);
|
||||
if (nameCb != null) {
|
||||
if (!authModule.isNameCallbackAllowed()) {
|
||||
RepositoryManager.log(null, null,
|
||||
"Illegal authentictaion callback: NameCallback not permitted",
|
||||
username);
|
||||
throw new LoginException("Illegal authentictaion callback");
|
||||
"Illegal authentication callback: NameCallback not permitted", username);
|
||||
throw new LoginException("Illegal authentication callback");
|
||||
}
|
||||
NameCallback nameCb = (NameCallback) cb;
|
||||
String name = nameCb.getName();
|
||||
if (name == null) {
|
||||
RepositoryManager.log(null, null,
|
||||
"Illegal authentictaion callback: NameCallback must specify login name",
|
||||
"Illegal authentication callback: NameCallback must specify login name",
|
||||
username);
|
||||
throw new LoginException("Illegal authentictaion callback");
|
||||
throw new LoginException("Illegal authentication callback");
|
||||
}
|
||||
username = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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<String> 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;
|
||||
|
@ -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...");
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
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]
|
||||
|
||||
-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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
-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.
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
* <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 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.
|
||||
* <p>
|
||||
* @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 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
try {
|
||||
pass = passCb.getPassword();
|
||||
char[] pass = passCb.getPassword();
|
||||
passCb.clearPassword();
|
||||
|
||||
try {
|
||||
userMgr.authenticateUser(username, pass);
|
||||
}
|
||||
catch (IOException e) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.*;
|
||||
|
@ -1214,4 +1215,30 @@ public final class FileUtilities {
|
|||
}
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
;
|
||||
};
|
||||
|
28
Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh
Executable file
28
Ghidra/RuntimeScripts/Common/server/jaas/jaas_external_program.example.sh
Executable 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
|
||||
|
|
@ -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"
|
||||
;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
|
@ -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 <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
|
||||
# -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<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)
|
||||
# -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
|
||||
# of reverse lookup by your DNS server)
|
||||
# ${ghidra.repositories.dir} : config variable (defined above) which identifies the directory
|
||||
|
|
|
@ -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|
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue