Skip to content

Commit

Permalink
Add support for dynamic socket factory classes
Browse files Browse the repository at this point in the history
remote-method-guesser now attempts to create missing socket factory
classes dynamically.
  • Loading branch information
qtc-de committed Sep 29, 2023
1 parent 0d2ff9a commit c4dd959
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 23 deletions.
44 changes: 35 additions & 9 deletions src/de/qtc/rmg/internal/CodebaseCollector.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@
* serialVersionUID. Since changing the serialVersionUID of an already existing class is not possible, we instead
* create a new class where the full qualified class name is prefixed with an underscore.
*
* From remote-method-guesser v4.5.0, this class has another purpose of handling custom socket factories. When the
* server exposes RMI objects with custom socket factory classes, this usually causes a ClassNotFound error, as
* we do not have the associated implementations on the client side. In this case, remote-method-guesser now attempts
* to create the socket factory class dynamically. Since the implementation is still unknown, it simply clones the
* default socket factory class TrustAllSocketFactory. This works surprisingly often, as most custom socket factory
* classes use simple socket implementations under the hood. This dynamic class creation is done for all classes that
* are unknown and contain "SocketFactory" within their class name or end with "Factory" or "SF". The user can also
* specify other patterns using the --socket-factory option.
*
* Summarized:
*
* 1. Extract server specified codebases and store them within a HashMap for later use
Expand Down Expand Up @@ -78,31 +87,48 @@ public class CodebaseCollector extends RMIClassLoaderSpi {
public Class<?> loadClass(String codebase, String name, ClassLoader defaultLoader) throws MalformedURLException, ClassNotFoundException
{
Class<?> resolvedClass = null;
long serialVersionUID = RMGOption.SERIAL_VERSION_UID.getValue();

addCodebase(codebase, name);
codebase = null;

if (serialVersionUIDMap.containsKey(name))
{
serialVersionUID = serialVersionUIDMap.get(name);
name = "_" + name;
}

try {

if (name.endsWith("_Stub"))
{
long serialVersionUID = RMGOption.SERIAL_VERSION_UID.getValue();
RMGUtils.makeLegacyStub(name, serialVersionUID);
}

else if (name.equals("sun.rmi.server.ActivatableRef"))
{
RMGUtils.makeActivatableRef();
}

if (serialVersionUIDMap.containsKey(name))
else if (!RMGOption.SOCKET_FACTORY.isNull())
{
if (name.contains(RMGOption.SOCKET_FACTORY.<String>getValue()))
{
serialVersionUID = serialVersionUIDMap.get(name);
name = "_" + name;
RMGUtils.makeSocketFactory(name, serialVersionUID);
}

RMGUtils.makeLegacyStub(name, serialVersionUID);
}

if (name.equals("sun.rmi.server.ActivatableRef"))
RMGUtils.makeActivatableRef();
else if (name.contains("SocketFactory") || name.endsWith("Factory") || name.endsWith("SF"))
{
RMGUtils.makeSocketFactory(name, serialVersionUID);
}

resolvedClass = originalLoader.loadClass(codebase, name, defaultLoader);

} catch (CannotCompileException | NotFoundException e) {
}

catch (CannotCompileException | NotFoundException e)
{
ExceptionHandler.internalError("loadClass", "Unable to compile unknown stub class.");
}

Expand Down
4 changes: 2 additions & 2 deletions src/de/qtc/rmg/internal/RMGOption.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ public enum RMGOption {
DGC_METHOD("--dgc-method", "method to use for dgc operations", Arguments.store(), RMGOptionGroup.ACTION, "method"),
REG_METHOD("--registry-method", "method to use for registry operations", Arguments.store(), RMGOptionGroup.ACTION, "method"),
SERIAL_VERSION_UID("--serial-version-uid", "serialVersionUID to use for RMI stubs", Arguments.store(), RMGOptionGroup.ACTION, "uid"),
PAYLOAD_SERIAL_VERSION_UID("--payload-serial-version-uid", "serialVersionUID to use for payload classes", Arguments.store(), RMGOptionGroup.ACTION, "uid");

PAYLOAD_SERIAL_VERSION_UID("--payload-serial-version-uid", "serialVersionUID to use for payload classes", Arguments.store(), RMGOptionGroup.ACTION, "uid"),
SOCKET_FACTORY("--socket-factory", "dynamically create a socket factory class with the specified name", Arguments.store(), RMGOptionGroup.ACTION, "classname");

public final String name;
public final String description;
Expand Down
47 changes: 35 additions & 12 deletions src/de/qtc/rmg/networking/RMIRegistryEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,39 +159,54 @@ public RemoteObjectWrapper lookup(String boundName) throws IllegalArgumentExcept
{
Remote remoteObject = remoteObjectCache.get(boundName);

if( remoteObject == null ) {

try {
if (remoteObject == null)
{
try
{
remoteObject = rmiRegistry.lookup(boundName);
remoteObjectCache.put(boundName, remoteObject);
}

} catch( java.rmi.ConnectIOException e ) {
catch (java.rmi.ConnectIOException e)
{
ExceptionHandler.connectIOException(e, "lookup");
}

} catch( java.rmi.ConnectException e ) {
catch (java.rmi.ConnectException e)
{
ExceptionHandler.connectException(e, "lookup");
}

} catch( java.rmi.UnknownHostException e ) {
catch (java.rmi.UnknownHostException e)
{
ExceptionHandler.unknownHost(e, host, true);
}

} catch( java.rmi.NoSuchObjectException e ) {
catch (java.rmi.NoSuchObjectException e)
{
ExceptionHandler.noSuchObjectException(e, "registry", true);
}

} catch( java.rmi.NotBoundException e ) {
catch (java.rmi.NotBoundException e)
{
ExceptionHandler.notBoundException(e, boundName);
}

} catch( Exception e ) {

catch( Exception e )
{
Throwable cause = ExceptionHandler.getCause(e);

if (e instanceof UnmarshalException && cause instanceof InvalidClassException)
{
InvalidClassException invalidClassException = (InvalidClassException)cause;

if (stopLookupLoop || ! cause.getMessage().contains("serialVersionUID"))
if (stopLookupLoop || !cause.getMessage().contains("serialVersionUID"))
{
ExceptionHandler.invalidClassException(invalidClassException);
}

try {
try
{
String className = RMGUtils.getClass(invalidClassException);
long serialVersionUID = RMGUtils.getSerialVersionUID(invalidClassException);

Expand All @@ -208,16 +223,24 @@ public RemoteObjectWrapper lookup(String boundName) throws IllegalArgumentExcept
}

else if (e instanceof UnmarshalException && e.getMessage().contains("Transport return code invalid"))
{
throw (UnmarshalException)e;
}

if( cause instanceof ClassNotFoundException )
{
ExceptionHandler.lookupClassNotFoundException(e, cause.getMessage());
}

else if( cause instanceof SSRFException )
{
SSRFSocket.printContent(host, port);
}

else
{
ExceptionHandler.unexpectedException(e, "lookup", "call", true);
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/de/qtc/rmg/operations/Operation.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public enum Operation {
RMGOption.BIND_GADGET_NAME,
RMGOption.BIND_GADGET_CMD,
RMGOption.YSO,
RMGOption.SOCKET_FACTORY,
}),

CALL("dispatchCall", "<arguments>", "Regularly calls a method with the specified arguments", new RMGOption[] {
Expand All @@ -69,6 +70,7 @@ public enum Operation {
RMGOption.CALL_ARGUMENTS,
RMGOption.FORCE_ACTIVATION,
RMGOption.SERIAL_VERSION_UID,
RMGOption.SOCKET_FACTORY,
}),

CODEBASE("dispatchCodebase", "<classname> <url>", "Perform remote class loading attacks", new RMGOption[] {
Expand Down Expand Up @@ -96,6 +98,7 @@ public enum Operation {
RMGOption.FORCE_ACTIVATION,
RMGOption.SERIAL_VERSION_UID,
RMGOption.PAYLOAD_SERIAL_VERSION_UID,
RMGOption.SOCKET_FACTORY,
}),

ENUM("dispatchEnum", "[scan-action ...]", "Enumerate common vulnerabilities on Java RMI endpoints", new RMGOption[] {
Expand All @@ -121,6 +124,7 @@ public enum Operation {
RMGOption.ACTIVATION,
RMGOption.FORCE_ACTIVATION,
RMGOption.SERIAL_VERSION_UID,
RMGOption.SOCKET_FACTORY,
}),

GUESS("dispatchGuess", "", "Guess methods on bound names", new RMGOption[] {
Expand Down Expand Up @@ -150,6 +154,7 @@ public enum Operation {
RMGOption.NO_PROGRESS,
RMGOption.FORCE_ACTIVATION,
RMGOption.SERIAL_VERSION_UID,
RMGOption.SOCKET_FACTORY,
}),

KNOWN("dispatchKnown", "<className>", "Display details of known remote objects", new RMGOption[] {
Expand Down Expand Up @@ -201,6 +206,7 @@ public enum Operation {
RMGOption.BIND_GADGET_NAME,
RMGOption.BIND_GADGET_CMD,
RMGOption.YSO,
RMGOption.SOCKET_FACTORY,
}),

ROGUEJMX("dispatchRogueJMX", "[forward-host]", "Creates a rogue JMX listener (collect credentials)", new RMGOption[] {
Expand Down Expand Up @@ -258,6 +264,7 @@ public enum Operation {
RMGOption.YSO,
RMGOption.FORCE_ACTIVATION,
RMGOption.SERIAL_VERSION_UID,
RMGOption.SOCKET_FACTORY,
}),

UNBIND("dispatchUnbind", "", "Removes the specified bound name from the registry", new RMGOption[] {
Expand All @@ -276,6 +283,7 @@ public enum Operation {
RMGOption.SSRF_STREAM_PROTOCOL,
RMGOption.BIND_BOUND_NAME,
RMGOption.BIND_BYPASS,
RMGOption.SOCKET_FACTORY,
});

private Method method;
Expand Down
39 changes: 39 additions & 0 deletions src/de/qtc/rmg/utils/RMGUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import de.qtc.rmg.internal.ExceptionHandler;
import de.qtc.rmg.internal.MethodArguments;
import de.qtc.rmg.internal.MethodCandidate;
import de.qtc.rmg.internal.RMGOption;
import de.qtc.rmg.internal.RMIComponent;
import de.qtc.rmg.io.Logger;
import de.qtc.rmg.io.MaliciousOutputStream;
Expand Down Expand Up @@ -212,6 +213,44 @@ public static Class makeActivatableRef() throws CannotCompileException
return null;
}

/**
* Dynamically create a socket factory class that implements RMIClientSocketFactory. This function is used when
* the RMI server uses a custom socket factory class. In this case, rmg attempts to connect with it's default
* TrustAllSocketFactory, which works if the custom socket factory provided by the server is not too different.
*
* To achieve this, rmg just clones TrustAllSocketFactory and assigns it a new name. As in the case of Stub
* classes with unusual serialVersionUIDs, the serialVersionUID is determined error based. The factory is first
* created using a default serialVersionUID. This should cause an exception revealing the actual serialVersionUID.
* This is then used to recreate the class.
*
* Check the CodebaseCollector class documentation for more information.
*
* @return socket factory class that implements RMIClientSocketFactory
* @throws CannotCompileException
*/
public static Class makeSocketFactory(String className, long serialVersionUID) throws CannotCompileException
{
try
{
return Class.forName(className);
}

catch (ClassNotFoundException e) {}

CtClass ctClass = null;

try
{
ctClass = pool.getAndRename("de.qtc.rmg.networking.TrustAllSocketFactory", className);
ctClass.addInterface(serializable);
addSerialVersionUID(ctClass, serialVersionUID);
}

catch (NotFoundException e) {}

return ctClass.toClass();
}

/**
* Creates a method from a signature string. Methods need to be assigned to a class, therefore the static
* dummyClass is used that is created during the initialization of RMGUtils. The class relationship of the
Expand Down

0 comments on commit c4dd959

Please sign in to comment.