diff --git a/Ghidra/Features/GhidraServer/certification.manifest b/Ghidra/Features/GhidraServer/certification.manifest index 0a7c81fcee..62e2ccc857 100644 --- a/Ghidra/Features/GhidraServer/certification.manifest +++ b/Ghidra/Features/GhidraServer/certification.manifest @@ -3,5 +3,6 @@ ##MODULE IP: LGPL 2.1 ##MODULE IP: Tango Icons - Public Domain Module.manifest||GHIDRA||||END| +data/serial.filter||GHIDRA||||END| os/readme.txt||GHIDRA||||END| src/main/java/ghidra/server/remote/ServerHelp.txt||GHIDRA||||END| diff --git a/Ghidra/Features/GhidraServer/data/serial.filter b/Ghidra/Features/GhidraServer/data/serial.filter new file mode 100644 index 0000000000..5dc35e3823 --- /dev/null +++ b/Ghidra/Features/GhidraServer/data/serial.filter @@ -0,0 +1,22 @@ +# Ghidra Server serialization filter patterns +# See java.io.ObjectInputFilter.Config#createFilter(String) +# +# This file establishes allowed and disallowed inbound class de-serialization +# rules for the Ghidra Server. If not specifically allowed or disallowed a +# de-serialized class will be subject to an internal filter which allows all +# primitive and primitive array classes while rejecting all other classes. +# + +java.base/java.lang.*; +java.base/java.security.**; +java.base/javax.security.**; +java.base/sun.security.**; +java.base/java.util.**; + +ghidra.framework.remote.GhidraPrincipal; +ghidra.framework.remote.AnonymousCallback; +ghidra.framework.remote.SSHSignatureCallback; +ghidra.framework.remote.SignatureCallback; +ghidra.framework.remote.User; +ghidra.framework.remote.User[]; +ghidra.framework.store.CheckoutType; 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 088c970198..f0bb2ad17e 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 @@ -66,7 +66,9 @@ import utility.application.ApplicationLayout; */ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHandle { - private final static String TLS_SERVER_PROTOCOLS_PROPERTY = "ghidra.tls.server.protocols"; + private static final String SERIAL_FILTER_FILE = "serial.filter"; + + private static final String TLS_SERVER_PROTOCOLS_PROPERTY = "ghidra.tls.server.protocols"; private static SslRMIServerSocketFactory serverSocketFactory; private static SslRMIClientSocketFactory clientSocketFactory; @@ -136,7 +138,9 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan * @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 + * @param jaasConfigFile JAAS configuration file + * @throws IOException if an IO error occurs + * @throws CertificateException if failed to parse CA certs file used for PKI authentication */ GhidraServer(File rootDir, AuthMode authMode, String loginDomain, boolean allowUserToSpecifyName, boolean altSSHLoginAllowed, @@ -203,6 +207,9 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan GhidraServer.server = this; + // Establish serialization filter to address deserialization vulnerabity concerns + setGlobalSerializationFilter(); + // Start block stream server - use RMI serverSocketFactory blockStreamServer = BlockStreamServer.getBlockStreamServer(); ServerSocket streamServerSocket; @@ -855,4 +862,117 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan return clientSocketFactory; } + private static void setGlobalSerializationFilter() throws IOException { + + ObjectInputFilter patternFilter = readSerialFilterPatternFile(); + + ObjectInputFilter filter = new ObjectInputFilter() { + + @Override + public Status checkInput(FilterInfo info) { + + Class clazz = info.serialClass(); + + // Give serial filter patterns first shot + Status status = patternFilter.checkInput(info); + if (status != Status.UNDECIDED) { + if (status == Status.REJECTED) { + return serialReject(info, "failed by serial.filter pattern"); + } + return status; + } + + + if (clazz == null) { + return Status.ALLOWED; + } + + Class componentType = clazz.getComponentType(); + if (componentType != null && componentType.isPrimitive()) { + return Status.ALLOWED; // allow all primitive arrays + } + + return serialReject(info, "not allowed"); + } + + private Status serialReject(FilterInfo info, String reason) { + String clientHost = RepositoryManager.getRMIClient(); + StringBuilder buf = new StringBuilder(); + buf.append("Rejected class serialization"); + if (clientHost != null) { + buf.append(" from "); + buf.append(clientHost); + } + buf.append("("); + buf.append(reason); + buf.append(")"); + + Class serialClass = info.serialClass(); + if (serialClass != null) { + buf.append(": "); + buf.append(serialClass.getCanonicalName()); + buf.append(" "); + if (serialClass.getComponentType() != null) { + buf.append("("); + buf.append("array-length="); + buf.append(info.arrayLength()); + buf.append(")"); + } + } + + log.error(buf.toString()); + return Status.REJECTED; + } + + }; + + // Install global serial class filter + ObjectInputFilter.Config.setSerialFilter(filter); + } + + /** + * Read serial.filter file content removing any comments and newlines and generate + * corresponding {@link ObjectInputFilter}. See {@link java.io.ObjectInputFilter.Config#createFilter(String)} + * for filter syntax. + * @return serial filter content + * @throws IOException if file error occurs + */ + private static ObjectInputFilter readSerialFilterPatternFile() throws IOException { + + File serialFilterFile = Application.getModuleDataFile(SERIAL_FILTER_FILE).getFile(false); + if (serialFilterFile == null) { + // jar mode not supported + throw new FileNotFoundException(SERIAL_FILTER_FILE + " not found"); + } + try { + StringBuilder buf = new StringBuilder(); + try (FileReader fr = new FileReader(serialFilterFile); + BufferedReader r = new BufferedReader(fr)) { + + for (String line = r.readLine(); line != null; line = r.readLine()) { + int ix = line.indexOf('#'); + if (ix >= 0) { + // strip comment + line = line.substring(0, ix); + } + line = line.trim(); + if (line.length() == 0) { + continue; + } + if (!line.endsWith(";")) { + throw new IllegalArgumentException( + "all filter statements must end with `;`"); + } + if (line.length() != 0) { + buf.append(line); + } + } + } + return ObjectInputFilter.Config.createFilter(buf.toString()); + } + catch (Exception e) { + throw new IOException("Failed to parse " + SERIAL_FILTER_FILE, e); + } + } + } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java index ad44b21f93..13beaf102b 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServerApplicationLayout.java @@ -17,12 +17,14 @@ package ghidra.server.remote; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import ghidra.framework.ApplicationProperties; import ghidra.util.SystemUtilities; import utility.application.ApplicationLayout; import utility.application.ApplicationUtilities; +import utility.module.ModuleUtilities; /** * The Ghidra server application layout defines the customizable elements of the Ghidra @@ -56,5 +58,10 @@ public class GhidraServerApplicationLayout extends ApplicationLayout { // User directories (don't let anything use the user home directory...there may not be one) userTempDir = ApplicationUtilities.getDefaultUserTempDir(applicationProperties); + + // Modules - required to find module data files + modules = ModuleUtilities.findModules(applicationRootDirs, + ModuleUtilities.findModuleRootDirectories(applicationRootDirs, new ArrayList<>())); + } } diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java new file mode 100644 index 0000000000..ac11e85b63 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java @@ -0,0 +1,129 @@ +/* ### + * 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.framework.client; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.InvalidClassException; +import java.rmi.RemoteException; +import java.rmi.UnmarshalException; +import java.security.Principal; +import java.util.HashSet; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; + +import org.junit.*; +import org.junit.experimental.categories.Category; + +import generic.test.category.PortSensitiveCategory; +import ghidra.framework.model.ServerInfo; +import ghidra.framework.remote.GhidraServerHandle; +import ghidra.net.ApplicationKeyManagerFactory; +import ghidra.server.remote.ServerTestUtil; +import ghidra.test.AbstractGhidraHeadlessIntegrationTest; +import utilities.util.FileUtilities; + +@Category(PortSensitiveCategory.class) +public class GhidraServerSerialFilterFailureTest extends AbstractGhidraHeadlessIntegrationTest { + + private File serverRoot; + + @Before + public void setUp() throws Exception { + System.clearProperty(ApplicationKeyManagerFactory.KEYSTORE_PATH_PROPERTY); + } + + @After + public void tearDown() throws Exception { + closeAllWindows(); + killServer(); + + ClientUtil.clearRepositoryAdapter("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); + } + + private void killServer() { + + if (serverRoot == null) { + return; + } + + ServerTestUtil.disposeServer(); + + FileUtilities.deleteDir(serverRoot); + } + + private void startServer(int authMode, boolean altLoginName, boolean enableSSH, + boolean enableAnonymous) throws Exception { + + // Create server instance + serverRoot = new File(getTestDirectoryPath(), "TestServer"); + + ServerTestUtil.startServer(serverRoot.getAbsolutePath(), + ServerTestUtil.GHIDRA_TEST_SERVER_PORT, authMode, altLoginName, enableSSH, + enableAnonymous); + } + + + static class BogusPrincipal implements Principal, java.io.Serializable { + + private String username; + + public BogusPrincipal(String username) { + this.username = username; + } + + @Override + public String getName() { + return username; + } + } + + private static Subject getBogusUserSubject() { + String username = ClientUtil.getUserName(); + HashSet pset = new HashSet<>(); + HashSet emptySet = new HashSet<>(); + pset.add(new BogusPrincipal(username)); + Subject subj = new Subject(false, pset, emptySet, emptySet); + return subj; + } + + @Test + public void testSerializationFailure() throws Exception { + + ServerTestUtil.setLocalUser("test"); + startServer(-1, false, false, false); + + ServerInfo server = new ServerInfo("localhost", ServerTestUtil.GHIDRA_TEST_SERVER_PORT); + + GhidraServerHandle serverHandle = ServerConnectTask.getGhidraServerHandle(server); + + try { + serverHandle.getRepositoryServer(getBogusUserSubject(), new Callback[0]); + fail("serial filter rejection failed to perform"); + } + catch (RemoteException e) { + Throwable cause = e.getCause(); + assertTrue("expected remote unmarshall exception", cause instanceof UnmarshalException); + cause = cause.getCause(); + assertTrue("expected remote invalid class exceptionn", + cause instanceof InvalidClassException); + } + + } + +}