diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c31067a..d8000005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The ``guess`` operation now also lists methods for known remote objects * These are obtained via reflection, not by guessing * You can force guessing anyway by using ``--force-guessing`` -* Method arguments are not marshalled correctly (previously, always writeObject was used) +* Method arguments are now marshalled correctly (previously, always writeObject was used) ## [3.1.1] - Feb 16, 2021 diff --git a/src/de/qtc/rmg/Starter.java b/src/de/qtc/rmg/Starter.java index 74429a95..75e4b3a6 100644 --- a/src/de/qtc/rmg/Starter.java +++ b/src/de/qtc/rmg/Starter.java @@ -5,6 +5,12 @@ import de.qtc.rmg.operations.Operation; import de.qtc.rmg.utils.RMGUtils; +/** + * The Starter class contains the entrypoint of remote-method-guesser. It is responsible + * for creating a Dispatcher object, that is used to dispatch the actual method call. + * + * @author Tobias Neitzel (@qtc_de) + */ public class Starter { public static void main(String[] argv) { diff --git a/src/de/qtc/rmg/annotations/Parameters.java b/src/de/qtc/rmg/annotations/Parameters.java index a35f5732..cfea0003 100644 --- a/src/de/qtc/rmg/annotations/Parameters.java +++ b/src/de/qtc/rmg/annotations/Parameters.java @@ -5,9 +5,24 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * remote-method-guesser uses commons-cli for argument parsing. While being a useful + * library, it misses support module based argument parsing and some other features that + * are available in more modern argument parsers. Nonetheless, currently we still stick to + * it and the Parameters annotation class is used to implement some additional argument + * checking. + * + * Each operation supported by the Dispatcher class can be marked by this annotation. + * The count attribute specifies how many positional arguments the corresponding operation + * expects. The requires attribute can be used to specify which options are required for the + * action. If only one action of a particular set is required (e.g. --bound-name or --objid), + * the following syntax can be used: "--bound-name|--objid". + * + * @author Tobias Neitzel (@qtc_de) + */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Parameters { - int count() default 0; - String[] requires() default {}; + int count() default 0; + String[] requires() default {}; } diff --git a/src/de/qtc/rmg/exceptions/MalformedPluginException.java b/src/de/qtc/rmg/exceptions/MalformedPluginException.java index d3e8d383..6d739203 100644 --- a/src/de/qtc/rmg/exceptions/MalformedPluginException.java +++ b/src/de/qtc/rmg/exceptions/MalformedPluginException.java @@ -1,5 +1,13 @@ package de.qtc.rmg.exceptions; +/** + * MalformedPluginExceptions are thrown then an rmg plugin was specified on the command + * line that does not satisfy the plugin requirements. Usually that happens then the + * Manifest of the corresponding plugin does not contain a reference to the rmg plugin + * class. + * + * @author Tobias Neitzel (@qtc_de) + */ public class MalformedPluginException extends Exception { private static final long serialVersionUID = 1L; diff --git a/src/de/qtc/rmg/exceptions/UnexpectedCharacterException.java b/src/de/qtc/rmg/exceptions/UnexpectedCharacterException.java index b33c53ad..c573ac8f 100644 --- a/src/de/qtc/rmg/exceptions/UnexpectedCharacterException.java +++ b/src/de/qtc/rmg/exceptions/UnexpectedCharacterException.java @@ -7,6 +7,10 @@ * filtering in this regard is very strict, but can be disabled by using the * --trusted switch after reviewing the corresponding names. * + * The reason for this filtering is simple: rmg uses the bound names from the + * RMI registry within of the file names for the sample files. Bound names can + * contain arbitrary characters, which includes e.g. path traversal sequences. + * * @author Tobias Neitzel (@qtc_de) */ @SuppressWarnings("serial") diff --git a/src/de/qtc/rmg/internal/ArgumentParser.java b/src/de/qtc/rmg/internal/ArgumentParser.java index 104dd525..8103235e 100644 --- a/src/de/qtc/rmg/internal/ArgumentParser.java +++ b/src/de/qtc/rmg/internal/ArgumentParser.java @@ -1,5 +1,8 @@ package de.qtc.rmg.internal; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; @@ -13,7 +16,6 @@ import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import de.qtc.rmg.Starter; import de.qtc.rmg.annotations.Parameters; import de.qtc.rmg.io.Logger; import de.qtc.rmg.operations.Operation; @@ -28,6 +30,9 @@ * argument parser, as the amount of options and actions has become quite * high. However, for now the current parsing should be sufficient. * + * To implement more complicated parsing logic, it is recommended to look at the + * de.qtc.rmg.annotations.Parameters class. + * * @author Tobias Neitzel (@qtc_de) */ public class ArgumentParser { @@ -43,11 +48,12 @@ public class ArgumentParser { private Properties config; private HashMap parameters; - private static String defaultConfiguration = "/config.properties"; + private String defaultConfiguration = "/config.properties"; /** - * Creates the actual parser object and initializes it with some default - * options. + * Creates the actual parser object and initializes it with some default options + * and parses the current command line. Parsed parameters are stored within the + * parameters HashMap. */ public ArgumentParser(String[] argv) { @@ -61,28 +67,76 @@ public ArgumentParser(String[] argv) this.parse(argv); } - private Object parseOption(String name, Object defaultValue) + /** + * Parses the specified command line arguments and handles some shortcuts. + * E.g. the --help and --trusted options are already caught at this level and + * set the corresponding global variables in other classes. + * + * @param argv arguments specified on the command line + */ + private void parse(String[] argv) { - Object returnValue = null; - try { - returnValue = cmdLine.getParsedOptionValue(name); + cmdLine = parser.parse(this.options, argv); + } catch (ParseException e) { + System.err.println("Error: " + e.getMessage() + "\n"); + printHelpAndExit(1); + } - if(returnValue instanceof Number) - returnValue = ((Number)returnValue).intValue(); + this.config = new Properties(); + loadConfig(cmdLine.getOptionValue("config", null)); - } catch(ParseException e) { - Logger.printlnPlainMixedYellow("Error: Invalid parameter type for argument", name); - System.out.println(""); - printHelpAndExit(1); + if( cmdLine.hasOption("help") ) { + printHelpAndExit(0); } - if( returnValue == null ) - return defaultValue; - else - return returnValue; + if( cmdLine.hasOption("trusted") ) + Security.trusted(); + + if( cmdLine.hasOption("no-color") ) + Logger.disableColor(); + + PluginSystem.init(cmdLine.getOptionValue("plugin", null)); + ExceptionHandler.showStackTrace(cmdLine.hasOption("stack-trace")); + YsoIntegration.setYsoPath(cmdLine.getOptionValue("yso", config.getProperty("ysoserial-path"))); + + preapreParameters(); + } + + /** + * Loads the remote-method-guesser configuration file from the specified destination. The default configuration + * is always loaded. If the filename parameter is not null, an additional user specified config is loaded, that + * may overwrites some configurations. + * + * @param filename file system path to load the configuration file from + */ + private void loadConfig(String filename) + { + try { + InputStream configStream = null; + + configStream = ArgumentParser.class.getResourceAsStream(defaultConfiguration); + config.load(configStream); + configStream.close(); + + if( filename != null ) { + configStream = new FileInputStream(filename); + config.load(configStream); + configStream.close(); + } + + } catch( IOException e ) { + ExceptionHandler.unexpectedException(e, "loading", ".properties file", true); + } } + + /** + * rmg stores command line parameters within a HashMap, that makes the syntax for obtaining parameter + * values a little bit shorter than using commons-cli syntax. This function creates the corresponding HashMap + * and fills it with the values parsed from the command line. Certain options also have default values assigned, + * that are used when the corresponding option was not specified. + */ private void preapreParameters() { parameters = new HashMap(); @@ -108,6 +162,52 @@ private void preapreParameters() parameters.put("force-guessing", cmdLine.hasOption("force-guessing")); } + /** + * This function is used for integer parsing. Commons-cli allows to restrict option values to certain parameter + * types. For numeric types, it uses the abstract Number class. Later on, rmg-functions consume such argument types + * usually as int and this function converts the value into this corresponding form. + * + * @param name parameter name within the command line + * @param defaultValue to use if the parameter is not present + * @return the parsing result that should be stored within the parameters HashMap + */ + private Object parseOption(String name, Object defaultValue) + { + Object returnValue = null; + + try { + returnValue = cmdLine.getParsedOptionValue(name); + + if(returnValue instanceof Number) + returnValue = ((Number)returnValue).intValue(); + + } catch(ParseException e) { + Logger.printlnPlainMixedYellow("Error: Invalid parameter type for argument", name); + System.out.println(""); + printHelpAndExit(1); + } + + if( returnValue == null ) + return defaultValue; + else + return returnValue; + } + + /** + * Returns the number of specified positional arguments. + * + * @return number of positional arguments. + */ + private int getArgumentCount() + { + if( this.argList != null ) { + return this.argList.size(); + } else { + this.argList = cmdLine.getArgList(); + return this.argList.size(); + } + } + /** * This function constructs all required parser options. rmg uses long options * only and does not define short versions for any option. @@ -247,14 +347,65 @@ private Options getParserOptions() } /** - * Returns the help string that is used by rmg. + * The validateOperation function is used to validate that all required parameters were specified for an operation. + * Each operation has one assigned method, that can be annotated with the Parameters annotation. If this annotation + * is present, this function checks the 'count' and 'requires' attribute and makes sure that the corresponding values + * have been specified on the command line. Read the de.qtc.rmg.annotations.Parameters class for more details. + * + * @param operation Operation to validate + */ + private void validateOperation(Operation operation) + { + Method m = operation.getMethod(); + Parameters paramRequirements = (Parameters)m.getAnnotation(Parameters.class); + + if(paramRequirements == null) + return; + + this.checkArgumentCount(paramRequirements.count()); + + for(String requiredOption: paramRequirements.requires()) { + + Object optionValue = null; + String[] possibleOptions = requiredOption.split("\\|"); + + for(String possibleOption: possibleOptions) { + if((optionValue = this.get(possibleOption)) != null) { + break; + } + } + + if(optionValue == null) { + + Logger.eprint("Error: The "); + + for(int ctr = 0; ctr < possibleOptions.length - 1; ctr++) { + Logger.printPlainYellow("--" + possibleOptions[ctr]); + + if(ctr == possibleOptions.length - 2) + Logger.printPlain(" or "); + + else + Logger.printPlain(", "); + } + + Logger.printPlainYellow("--" + possibleOptions[possibleOptions.length - 1]); + Logger.printlnPlainMixedBlue(" option is required for the", operation.toString().toLowerCase(), "operation."); + RMGUtils.exit(); + } + } + } + + /** + * Returns the help string that is used by rmg. The version information is read from the pom.xml, which makes + * it easier to keep it in sync. Possible operation names are taken from the Operation enumeration. * * @return help string. */ private String getHelpString() { String helpString = "rmg [options] \n\n" - +"rmg v" + Starter.class.getPackage().getImplementationVersion() + +"rmg v" + ArgumentParser.class.getPackage().getImplementationVersion() +" - Identify common misconfigurations on Java RMI endpoints.\n\n" +"Positional Arguments:\n" +" ip IP address of the target\n" @@ -264,7 +415,7 @@ private String getHelpString() for(Operation op : Operation.values()) { helpString += " " - + padRight(op.toString().toLowerCase() + " " + op.getArgs(), 32) + + Logger.padRight(op.toString().toLowerCase() + " " + op.getArgs(), 32) + op.getDescription() + "\n"; } @@ -272,62 +423,23 @@ private String getHelpString() return helpString; } - private String padRight(String s, int n) { - return String.format("%-" + n + "s", s); - } - /** - * Parses the specified command line arguments and handles some shortcuts. - * The --help option and --trusted are already caught at this level and - * do not need to be processed later on. + * Utility function to show the program help and exit it right away. * - * @param argv arguments specified on the command line - * @return + * @param code return code of the program */ - public void parse(String[] argv) - { - try { - cmdLine = parser.parse(this.options, argv); - } catch (ParseException e) { - System.err.println("Error: " + e.getMessage() + "\n"); - printHelpAndExit(1); - } - - this.config = new Properties(); - RMGUtils.loadConfig(defaultConfiguration, config, false); - RMGUtils.loadConfig(cmdLine.getOptionValue("config", null), config, true); - - if( cmdLine.hasOption("help") ) { - printHelpAndExit(0); - } - - if( cmdLine.hasOption("trusted") ) - Security.trusted(); - - if( cmdLine.hasOption("no-color") ) - Logger.disableColor(); - - PluginSystem.init(cmdLine.getOptionValue("plugin", null)); - ExceptionHandler.showStackTrace(cmdLine.hasOption("stack-trace")); - YsoIntegration.setYsoPath(cmdLine.getOptionValue("yso", config.getProperty("ysoserial-path"))); - - preapreParameters(); - } - - /** - * Prints the help menu of rmg. - */ - public void printHelp() - { - formatter.printHelp(helpString, options); - } - - public void printHelpAndExit(int code) + private void printHelpAndExit(int code) { formatter.printHelp(helpString, options); System.exit(code); } + /** + * This is only a wrapper around this.parameters.get to make parameters more accessible. + * + * @param name parameter name to obtain + * @return corresponding parameter value + */ public Object get(String name) { return parameters.get(name); @@ -375,20 +487,6 @@ public int getPositionalInt(int position) return 0; } - /** - * Returns the number of specified positional arguments. - * - * @return number of positional arguments. - */ - public int getArgumentCount() - { - if( this.argList != null ) { - return this.argList.size(); - } else { - this.argList = cmdLine.getArgList(); - return this.argList.size(); - } - } /** * Checks whether the specified amount of positional arguments is sufficiently high. @@ -431,8 +529,12 @@ else if( this.cmdLine.hasOption("--force-legacy") ) } /** - * Simply returns the specified action on the command line. If no action was specified, returns - * 'enum' as the default action. + * This function is used to check the requested operation name on the command line and returns the corresponding + * Operation object. If no operation was specified, the Operation.ENUM is used. Operations are always validated + * before they are returned. The validation checks if all required parameters for the corresponding operation were + * specified. + * + * @return Operation requested by the client */ public Operation getAction() { @@ -464,7 +566,7 @@ else if( this.getArgumentCount() < 3 ) { * @param regMethod requested by the user. * @return regMethod if valid. */ - public String validateRegMethod() + public String getRegMethod() { String regMethod = (String)this.get("reg-method"); @@ -485,7 +587,7 @@ public String validateRegMethod() * @param dgcMethod requested by the user. * @return dgcMethod if valid. */ - public String validateDgcMethod() + public String getDgcMethod() { String dgcMethod = (String)this.get("dgc-method"); @@ -516,16 +618,32 @@ public boolean containsMethodSignature() return !signature.matches("reg|dgc|act"); } + /** + * Utility function that returns the hostname specified on the command line. + * + * @return user specified hostname (target) + */ public String getHost() { return this.getPositionalString(0); } + /** + * Utility function that returns the port specified on the command line. + * + * @return user specified port (target) + */ public int getPort() { return this.getPositionalInt(1); } + /** + * Parses the user specified gadget arguments to request a corresponding gadget from the PayloadProvider. + * The corresponding gadget object is returned. + * + * @return gadget object build from the user specified arguments + */ public Object getGadget() { String gadget = this.getPositionalString(3); @@ -537,51 +655,15 @@ public Object getGadget() return PluginSystem.getPayloadObject(this.getAction(), gadget, command); } + /** + * Parses the user specified argument string during a call action. Passes the string to the corresponding + * ArgumentProvider and returns the result argument array. + * + * @return Object array resulting from the specified argument string + */ public Object[] getCallArguments() { String argumentString = this.getPositionalString(3); return PluginSystem.getArgumentArray(argumentString); } - - public void validateOperation(Operation operation) - { - Method m = operation.getMethod(); - Parameters paramRequirements = (Parameters)m.getAnnotation(Parameters.class); - - if(paramRequirements == null) - return; - - this.checkArgumentCount(paramRequirements.count()); - - for(String requiredOption: paramRequirements.requires()) { - - Object optionValue = null; - String[] possibleOptions = requiredOption.split("\\|"); - - for(String possibleOption: possibleOptions) { - if((optionValue = this.get(possibleOption)) != null) { - break; - } - } - - if(optionValue == null) { - - Logger.eprint("Error: The "); - - for(int ctr = 0; ctr < possibleOptions.length - 1; ctr++) { - Logger.printPlainYellow("--" + possibleOptions[ctr]); - - if(ctr == possibleOptions.length - 2) - Logger.printPlain(" or "); - - else - Logger.printPlain(", "); - } - - Logger.printPlainYellow("--" + possibleOptions[possibleOptions.length - 1]); - Logger.printlnPlainMixedBlue(" option is required for the", operation.toString().toLowerCase(), "operation."); - RMGUtils.exit(); - } - } - } } diff --git a/src/de/qtc/rmg/internal/MethodCandidate.java b/src/de/qtc/rmg/internal/MethodCandidate.java index 8bafa29d..71a57a27 100644 --- a/src/de/qtc/rmg/internal/MethodCandidate.java +++ b/src/de/qtc/rmg/internal/MethodCandidate.java @@ -103,24 +103,47 @@ public MethodCandidate(CtMethod method) throws NotFoundException } /** - * Two MethodCandidates are equal when their method hash is equal. + * Computes the RMI method hash over a CtMethod. + * + * @param method CtMethod to calculate the hash from + * @return RMI method hash */ - @Override - public boolean equals(Object o) + private long getCtMethodHash(CtMethod method) { - if(!(o instanceof MethodCandidate)) - return false; - - MethodCandidate other = (MethodCandidate)o; - return this.hash == other.getHash(); + String methodSignature = method.getName() + method.getSignature(); + return computeMethodHash(methodSignature); } /** - * MethodCandidates are hashed according to their method hash. + * Computes the RMI method hash from a method signature. This function was basically + * copied from https://github.com/waderwu/attackRmi and is therefore licensed under the + * Apache-2.0 License. + * + * @param methodSignature signature to compute the hash on + * @return RMI method hash */ - @Override - public int hashCode(){ - return Long.hashCode(this.hash); + private long computeMethodHash(String methodSignature) { + long hash = 0; + ByteArrayOutputStream sink = new ByteArrayOutputStream(127); + try { + MessageDigest md = MessageDigest.getInstance("SHA"); + DataOutputStream out = new DataOutputStream(new DigestOutputStream(sink, md)); + + out.writeUTF(methodSignature); + + // use only the first 64 bits of the digest for the hash + out.flush(); + byte hasharray[] = md.digest(); + for (int i = 0; i < Math.min(8, hasharray.length); i++) { + hash += ((long) (hasharray[i] & 0xFF)) << (i * 8); + } + } catch (IOException ignore) { + /* can't happen, but be deterministic anyway. */ + hash = -1; + } catch (NoSuchAlgorithmException complain) { + throw new SecurityException(complain.getMessage()); + } + return hash; } /** @@ -147,6 +170,8 @@ public MethodArguments getConfusedArgument() } /** + * Returns the parameter types of the method as obtained from the CtMethod. + * * @return the parameter types for the method * @throws CannotCompileException should never occur * @throws NotFoundException should never occur @@ -157,6 +182,9 @@ public CtClass[] getParameterTypes() throws CannotCompileException, NotFoundExce } /** + * Obtain the name of the corresponding method. If the CtMethod was not created so far, + * the function returns the placeholder "method". + * * @return the name of the method * @throws CannotCompileException should never occur * @throws NotFoundException should never occur @@ -170,51 +198,6 @@ public String getName() throws CannotCompileException, NotFoundException return "method"; } - /** - * Computes the RMI method hash over an CtMethod. - * - * @param method CtMethod to calculate the hash from - * @return RMI method hash - */ - private long getCtMethodHash(CtMethod method) - { - String methodSignature = method.getName() + method.getSignature(); - return computeMethodHash(methodSignature); - } - - // copied from https://github.com/waderwu/attackRmi - License: Apache-2.0 License - /** - * Computes the RMI method hash from a method signature. This function was basically - * copied from https://github.com/waderwu/attackRmi and is therefore licensed under the - * Apache-2.0 License. - * - * @param methodSignature signature to compute the hash on - * @return RMI method hash - */ - private long computeMethodHash(String methodSignature) { - long hash = 0; - ByteArrayOutputStream sink = new ByteArrayOutputStream(127); - try { - MessageDigest md = MessageDigest.getInstance("SHA"); - DataOutputStream out = new DataOutputStream(new DigestOutputStream(sink, md)); - - out.writeUTF(methodSignature); - - // use only the first 64 bits of the digest for the hash - out.flush(); - byte hasharray[] = md.digest(); - for (int i = 0; i < Math.min(8, hasharray.length); i++) { - hash += ((long) (hasharray[i] & 0xFF)) << (i * 8); - } - } catch (IOException ignore) { - /* can't happen, but be deterministic anyway. */ - hash = -1; - } catch (NoSuchAlgorithmException complain) { - throw new SecurityException(complain.getMessage()); - } - return hash; - } - /** * Searches the current MethodCandidate for non primitive arguments (yes, the name is misleading). * Non primitive arguments are required for deserialization attacks. If a non primitive argument is @@ -264,21 +247,41 @@ public int getPrimitive(int selected) throws NotFoundException, CannotCompileExc return result; } + /** + * Returns the current value of the signature attribute. + * + * @return The methods signature. + */ public String getSignature() { return this.signature; } + /** + * Returns the current value of the hash attribute. + * + * @return hash value of the method + */ public long getHash() { return this.hash; } + /** + * Returns the current value of the isPrimitive attribute. + * + * @return true if first argument within the method is a primitive + */ public boolean isPrimitive() { return this.isPrimitive; } + /** + * Returns the current value of the isVoid attribute. + * + * @return true if method does not take arguments, false otherwise + */ public boolean isVoid() { return this.isVoid; @@ -327,4 +330,25 @@ public String getArgumentTypeName(int position) return typeName; } + + /** + * Two MethodCandidates are equal when their method hash is equal. + */ + @Override + public boolean equals(Object o) + { + if(!(o instanceof MethodCandidate)) + return false; + + MethodCandidate other = (MethodCandidate)o; + return this.hash == other.getHash(); + } + + /** + * MethodCandidates are hashed according to their method hash. + */ + @Override + public int hashCode(){ + return Long.hashCode(this.hash); + } } diff --git a/src/de/qtc/rmg/internal/Pair.java b/src/de/qtc/rmg/internal/Pair.java index 5e813b53..5250e7b1 100644 --- a/src/de/qtc/rmg/internal/Pair.java +++ b/src/de/qtc/rmg/internal/Pair.java @@ -1,5 +1,14 @@ package de.qtc.rmg.internal; +/** + * For the MethodArhuments class, a Pair type is required. Unfortunately, Java 8 does not support such a + * type natively. This class is a very basic implementation that fulfills the requirements. + * + * @author Tobias Neitzel (@qtc_de) + * + * @param type of left + * @param type of right + */ public class Pair { private K left; diff --git a/src/de/qtc/rmg/io/Formatter.java b/src/de/qtc/rmg/io/Formatter.java index 876e6114..47881034 100644 --- a/src/de/qtc/rmg/io/Formatter.java +++ b/src/de/qtc/rmg/io/Formatter.java @@ -22,36 +22,18 @@ @SuppressWarnings({ "unchecked", "rawtypes" }) public class Formatter { - /** - * Basically a wrapper for the listBoundNames functions specified below. - * - * @param boundNames list of available bound names - * @param classes known and unknown classes of the corresponding bound names - */ - public void listBoundNames(ArrayList> classes) - { - HashMap knownClasses = null; - HashMap unknownClasses = null; - - if(classes != null) { - knownClasses = classes.get(0); - unknownClasses = classes.get(1); - } - - this.listBoundNames(knownClasses, unknownClasses); - } - /** * Creates a formatted list of available bound names and their corresponding classes. Classes * are divided in known classes (classes that are available on the current class path) and * unknown classes (not available on the current class path). * - * @param boundNames list of available bound names - * @param knownClasses list of known classes - * @param unknownClasses list of unknown classes + * @param classes array of maps containing boundname-classes pairs */ - public void listBoundNames(HashMap knownClasses, HashMap unknownClasses) + public void listBoundNames(ArrayList> classes) { + HashMap knownClasses = classes.get(0); + HashMap unknownClasses = classes.get(1); + Logger.printlnBlue("RMI registry bound names:"); Logger.println(""); Logger.increaseIndent(); diff --git a/src/de/qtc/rmg/io/Logger.java b/src/de/qtc/rmg/io/Logger.java index 8e9dbb08..3a36f430 100644 --- a/src/de/qtc/rmg/io/Logger.java +++ b/src/de/qtc/rmg/io/Logger.java @@ -490,4 +490,8 @@ public static void printGadgetCallIntro(String endpointName) Logger.println(""); Logger.increaseIndent(); } + + public static String padRight(String s, int n) { + return String.format("%-" + n + "s", s); + } } diff --git a/src/de/qtc/rmg/io/SampleWriter.java b/src/de/qtc/rmg/io/SampleWriter.java index 10caef69..3b19c474 100644 --- a/src/de/qtc/rmg/io/SampleWriter.java +++ b/src/de/qtc/rmg/io/SampleWriter.java @@ -44,7 +44,8 @@ public class SampleWriter { /** * Creates a SmapleWriter object. During the creation, a samples folder may be generated if not - * already present. The template folder needs to already exist for the creation of this object. + * already present. If the specified template folder is null or empty, rmg defaults to use it's + * internal template folder that is packed into the JAR file. * * @param templateFolder folder where template files are stored * @param sampleFolder folder where created samples should be created @@ -72,7 +73,9 @@ public SampleWriter(String templateFolder, String sampleFolder, boolean ssl, boo } /** - * Reads a template from the template folder and returns the corresponding content. + * Reads a template from the template folder and returns the corresponding content. Depending + * on the contents of this.templateFolder, an external template folder or the internal from the + * JAR file is used. * * @param templateName name of the template file * @return template content @@ -88,7 +91,9 @@ public String loadTemplate(String templateName) throws IOException } /** - * Reads a template from the template folder and returns the corresponding content. + * Reads a template file form the internal template folder and returns it's contents. As the + * internal template folder is contained within the JAR file, getResourceAsStream is used to + * load the template. * * @param templateName name of the template file * @return template content @@ -103,7 +108,8 @@ public String loadTemplateStream(String templateName) throws IOException } /** - * Reads a template from the template folder and returns the corresponding content. + * Reads a template from the template folder and returns the corresponding content. This function is + * called when external template folders are used. * * @param templateName name of the template file * @return template content @@ -122,6 +128,9 @@ public String loadTemplateFile(String templateName) throws IOException return new String(Files.readAllBytes(templateFile.toPath())); } + /** + * Wrapper around writeSamples with additional subfolder argument. + */ public void writeSample(String sampleFolder, String sampleName, String sampleContent) throws UnexpectedCharacterException, IOException { writeSample(sampleFolder, sampleName, sampleContent, null); diff --git a/src/de/qtc/rmg/io/WordlistHandler.java b/src/de/qtc/rmg/io/WordlistHandler.java index 2bf02f24..496f4080 100644 --- a/src/de/qtc/rmg/io/WordlistHandler.java +++ b/src/de/qtc/rmg/io/WordlistHandler.java @@ -46,7 +46,8 @@ public WordlistHandler(String wordlistFile, String wordlistFolder, boolean updat /** * Read the specified wordlist and return the corresponding MethodCandidates. Only uses a wordlist - * file, if one was specified. Otherwise, it searches the specified wordlist folder. + * file, if one was specified. Otherwise, it searches the specified wordlist folder. If there is also + * no wordlist folder specified, it defaults to the internal wordlists that are stored within the JAR. * * @return HashSet of MethodCandidates build from the wordlist * @throws IOException if an IO operation fails @@ -64,6 +65,14 @@ public HashSet getWordlistMethods() throws IOException } } + /** + * This function is responsible for loading internal wordlist files. These are stored within the JAR file + * in the wordlists folder on the top level of the archive. Enumerating files within an internal JAR folder + * is currently a pain and the available wordlist names are hardcoded into this class. + * + * @return HashSet of method candidates parsed from the wordlist file + * @throws IOException + */ public static HashSet getWordlistMethodsFromStream() throws IOException { HashSet methods = new HashSet(); @@ -87,7 +96,9 @@ public static HashSet getWordlistMethodsFromStream() throws IOE /** * Reads all files ending with .txt within the wordlist folder and returns the corresponding MethodCandidates. * - * @return HashSet of MethodCandidates build from the wordlist files + * @param folder wordlist folder to read the wordlist files from + * @param updateWordlists determiens whether wordlists should be updated after creating MethodCandidates + * @return HashSet of MethodCandidates parsed from the wordlist files * @throws IOException if an IO operation fails */ public static HashSet getWordlistMethodsFromFolder(String folder, boolean updateWordlists) throws IOException @@ -115,8 +126,9 @@ public static HashSet getWordlistMethodsFromFolder(String folde * be the advanced format. Otherwise, we have an unknown format and print a warning. If updateWordlists was set within the * constructor, each wordlist file is updated to the advanced format after the parsing. * - * @param file wordlist file to read in - * @return HashSet of MethodCandidates build from the wordlist file + * @param filename wordlist file to parse + * @param updateWordlists determiens whether wordlists should be updated after creating MethodCandidates + * @return HashSet of MethodCandidates parsed from the wordlist file * @throws IOException if an IO operation fails */ public static HashSet getWordlistMethodsFromFile(String filename, boolean updateWordlists) throws IOException @@ -139,13 +151,11 @@ public static HashSet getWordlistMethodsFromFile(String filenam } /** - * Parses a wordlist file for available methods and creates the corresponding MethodCandidates. Comments prefixed with '#' - * within wordlist files are ignored. Each non comment line is split on the ';' character. If the split has a length of 1, - * the ordinary wordlist format (that just contains the method signature) is assumed. If the length is 4 instead, it should - * be the advanced format. Otherwise, we have an unknown format and print a warning. If updateWordlists was set within the - * constructor, each wordlist file is updated to the advanced format after the parsing. + * Takes the contents of a wordlist file as a list of lines and parses the corresponding method specifications. + * Empty lines and lines starting with a '#' are ignored. All other lines should be valid method signatures and + * are parsed to MethodCandidates by this function. * - * @param file wordlist file to read in + * @param lines method signatures read from a wordlist file * @return HashSet of MethodCandidates build from the wordlist file * @throws IOException if an IO operation fails */ diff --git a/src/de/qtc/rmg/networking/RMIWhisperer.java b/src/de/qtc/rmg/networking/RMIWhisperer.java index 6aab9197..f5a70bbe 100644 --- a/src/de/qtc/rmg/networking/RMIWhisperer.java +++ b/src/de/qtc/rmg/networking/RMIWhisperer.java @@ -127,7 +127,7 @@ public void locateRegistry() /** * Obtains a list of bound names. This is basically a wrapper around the list function of the RMI registry, - * but it has error handling implemented. + * but has error handling implemented. * * @return list of available bound names. */ @@ -190,8 +190,7 @@ public String[] getBoundNames() throws java.rmi.NoSuchObjectException /** * When called with null as parameter, this function just obtains a list of bound names from the registry. - * If a string was specified instead, it returns a list that only contains that String. Not very useful, - * but it allows to write the main function of rmg a little bit more straigt forward. + * If a string was specified instead, it returns a list that only contains that String. * * @param boundName specified by the user. Is simply returned if not null * @return list of bound names or the user specified bound name @@ -272,17 +271,6 @@ public ArrayList> getClassNames(String[] boundNames) return returnList; } - /** - * Constructs a TCPEndpoint (class used by internal RMI communication) using the specified - * host, port and csf values. - * - * @return newly constructed TCPEndpoint - */ - public TCPEndpoint getEndpoint() - { - return new TCPEndpoint(host, port, csf, null); - } - /** * Constructs a RemoteRef (class used by internal RMI communication) using the specified * host, port, csf and objID values.. @@ -291,7 +279,7 @@ public TCPEndpoint getEndpoint() */ public RemoteRef getRemoteRef(ObjID objID) { - Endpoint endpoint = this.getEndpoint(); + Endpoint endpoint = new TCPEndpoint(host, port, csf, null); return new UnicastRef(new LiveRef(objID, endpoint, false)); } @@ -320,29 +308,39 @@ public void genericCall(ObjID objID, int callID, long methodHash, MethodArgument } /** - * Dispatches a raw RMI call. Having such a function available is important for some low level RMI operations like the localhost bypass or even - * just calling the registry with serialization gadgets. This method provides full access to almost all relevant parts of the actual RMI calls. + * Dispatches a raw RMI call. Having such a function available is important for some low level RMI operations like + * the localhost bypass or even just calling the registry with serialization gadgets. This method provides full + * access to almost all relevant parts of the actual RMI calls. * - * The target remote objects can be either specified by their ObjID or by using an already existing RemoteRef. The first approach is suitable - * for communicating with well known RMI objects like the registry, the DGC or the Activator. The second approach can be useful, when you just - * looked up an object using regular RMI functions and now want to dispatch a raw RMI call to the already obtain RemoteObject. + * The target remote objects can be either specified by their ObjID or by using an already existing RemoteRef. The + * first approach is suitable for communicating with well known RMI objects like the registry, the DGC or the Activator. + * The second approach can be useful, when you just looked up an object using regular RMI functions and now want to + * dispatch a raw RMI call to the already obtain RemoteObject. * - * Within the current RMI protocol, you invoke methods by specifying an ObjID to identify the RemoteObject you want to talk with and a method hash - * to identify the method you want to invoke. In legacy RMI, methods were instead identified by using a callID. This callID is basically the position - * of the method within the class definition and is therefore a positive number for legacy RMI calls. Within modern RMI, this method should be always - * negative (except when attempting localhost bypasses :P). The currently used method hash is replaced by an interface hash in the legacy implementation. + * Within the current RMI protocol, you invoke methods by specifying an ObjID to identify the RemoteObject you want to + * talk with and a method hash to identify the method you want to invoke. In legacy RMI, methods were instead identified + * by using a callID. This callID is basically the position of the method within the class definition and is therefore a + * positive number for legacy RMI calls. Within modern RMI, this method should be always negative (except when attempting + * localhost bypasses :P). The currently used method hash is replaced by an interface hash in the legacy implementation. + * The internal RMI communication (DGC and Registry) still use the legacy calling convention today, as you can check by + * searching for the corresponding Skeleton classes within the Java RMI source code. * - * The internal RMI communication (DGC and Registry) still use the legacy calling convention today, as you can check by searching for the corresponding - * Skeleton classes within the Java RMI source code. + * By default, the genericCall function just ignores responses from the RMI server. Responses are only parsed if an + * expected return type was specified during the function call and a ResponseHandler was registered by using the plugin + * system. * - * @param objID identifies the RemoteObject you want to communicate with. Registry = 0, Activator = 1, DGC = 2 or custom once... - * @param callID callID that is used for legacy calls. Basically specifies the position of the method + * @param objID identifies the RemoteObject you want to communicate with. Registry = 0, Activator = 1, DGC = 2 or + * custom once... + * @param callID callID that is used for legacy calls. Basically specifies the position of the method to call in legacy + * rmi calls. For current calling convention, it should be negative * @param methodHash hash value of the method to call or interface hash for legacy calls * @param callArguments map of arguments for the call. Each argument must also ship a class it desires to be serialized to * @param locationStream if true, uses the MaliciousOutpuStream class to write custom annotation objects * @param callName the name of the RMI call you want to dispatch (only used for logging) - * @param remoteRef optional remote reference to use for the call. If null, the specified ObjID and the host and port of this class are used - * @param rtype return type of the remote method. If specified, the servers response is forwarded to the ResponseHandler plugin + * @param remoteRef optional remote reference to use for the call. If null, the specified ObjID and the host and port + * of this class are used + * @param rtype return type of the remote method. If specified, the servers response is forwarded to the ResponseHandler + * plugin (if registered to the plugin system) * @throws Exception connection related exceptions are caught, but anything what can go wrong on the server side is thrown */ @SuppressWarnings({ "deprecation", "rawtypes" }) @@ -429,6 +427,17 @@ public void genericCall(ObjID objID, int callID, long methodHash, MethodArgument } } + /** + * Marshals the specified object value to the corresponding type and writes it to the specified + * output stream. This is basically a copy from the default RMI implementation of this function. + * The type values are obtained by the method signature and the object values come from the argument + * array. + * + * @param type data type to marshal to + * @param value object to be marshalled + * @param out output stream to marshal to + * @throws IOException in case of a failing write operation to the stream + */ private static void marshalValue(Class type, Object value, ObjectOutput out) throws IOException { if (type.isPrimitive()) { @@ -456,6 +465,18 @@ private static void marshalValue(Class type, Object value, ObjectOutput out) } } + /** + * Unmarshals an object from the specified ObjectInput according to the data type specified + * in the type parameter. This is required to read the result of RMI calls, as different types + * are written differently to the ObjectInput by the RMI server. The expected type is taken from + * the return value of the method signature. + * + * @param type data type that is expected from the stream + * @param in ObjectInput to read from. + * @return unmarshalled object + * @throws IOException if reading the ObjectInput fails + * @throws ClassNotFoundException if the read in class is unknown. + */ private static Object unmarshalValue(CtClass type, ObjectInput in) throws IOException, ClassNotFoundException { if (type.isPrimitive()) { diff --git a/src/de/qtc/rmg/operations/ActivationClient.java b/src/de/qtc/rmg/operations/ActivationClient.java index 4529682b..34ce0e13 100644 --- a/src/de/qtc/rmg/operations/ActivationClient.java +++ b/src/de/qtc/rmg/operations/ActivationClient.java @@ -237,6 +237,13 @@ public void codebaseCall(Object payloadObject) } } + /** + * Helper method to pack the arguments for the activate call. The first parameter of the corresponding remote method + * is the non primitive and contains the payload object. The second one is a boolean and always contains failse. + * + * @param payloadObject payload to use for the first non primitive argument + * @return MethodArguments object that can be used for the activate call + */ private MethodArguments prepareCallArguments(Object payloadObject) { MethodArguments callArguments = new MethodArguments(2); @@ -244,6 +251,7 @@ private MethodArguments prepareCallArguments(Object payloadObject) callArguments.add(false, boolean.class); return callArguments; } + /** * Implementation of the activate call. Just uses the genericCall function of the RMIWhisperer, which allows to perform * raw RMI calls. The activator is not implemented as a skeleton and already uses the new calling convention. As it only diff --git a/src/de/qtc/rmg/operations/DGCClient.java b/src/de/qtc/rmg/operations/DGCClient.java index 8e1d5879..a72f4aac 100644 --- a/src/de/qtc/rmg/operations/DGCClient.java +++ b/src/de/qtc/rmg/operations/DGCClient.java @@ -162,7 +162,7 @@ public void enumJEP290(String callName) /** * Invokes a DGC method with a user controlled codebase as class annotation. The codebase is already set - * by the ArgumentParser during the startup of the program. This method was never successfuly tested, as + * by the ArgumentParser during the startup of the program. This method was never successfully tested, as * it is difficult to find a Java version that is still vulnerable to this :D * * @param callName the DGC call to use for the operation (clean|dirty) @@ -271,7 +271,7 @@ private void dgcCall(String callName, MethodArguments callArguments, boolean mal } /** - * Looks up the callID for the specified DGC call. + * Looks up the callID for the specified DGC call. DGC endpoints only support the methods clean and dirty. * * @param callName the DGC call to use for the operation (clean|dirty) * @return callID for the corresponding call @@ -297,7 +297,7 @@ private int getCallByName(String callName) * * @param callName the DGC call to use for the operation (clean|dirty) * @param payloadObject object to use during the DGC call - * @return argument array that can be used for the corresponding call + * @return MethodArguments that can be used for the corresponding call */ private MethodArguments packArgsByName(String callName, Object payloadObject) { diff --git a/src/de/qtc/rmg/operations/Dispatcher.java b/src/de/qtc/rmg/operations/Dispatcher.java index 06db2959..6258da7f 100644 --- a/src/de/qtc/rmg/operations/Dispatcher.java +++ b/src/de/qtc/rmg/operations/Dispatcher.java @@ -22,6 +22,18 @@ import javassist.CannotCompileException; import javassist.NotFoundException; +/** + * The dispatcher class contains all method definitions for the different rmg actions. It obtains a reference + * to the ArgumentParser object and extracts all required arguments parameters for the corresponding method calls. + * + * Methods within the Dispatcher class can be annotated with the Parameters annotation to specify additional requirements + * on their expected arguments. Refer to the de.qtc.rmg.annotations.Parameters class for more details. + * + * To add a new operation to rmg, the operation must first be registered within the de.qtc.rmg.operations.Operation class. + * A new Operation needs to be created there that references the corresponding method within this class. + * + * @author Tobias Neitzel (@qtc_de) + */ public class Dispatcher { private RMIWhisperer rmi; @@ -32,6 +44,11 @@ public class Dispatcher { private HashMap allClasses = null; private ArrayList> boundClasses = null; + /** + * Creates the dispatcher object. + * + * @param p ArgumentParser object that contains the current command line specifications + */ public Dispatcher(ArgumentParser p) { this.p = p; @@ -41,6 +58,13 @@ public Dispatcher(ArgumentParser p) this.createMethodCandidate(); } + /** + * Obtains a list of bound names from the RMI registry. Additionally, each object is looked up to obtain + * the name of the class that it implements. All results are saved within of class variables within the + * Dispatcher class. + * + * @throws java.rmi.NoSuchObjectException is thrown when the specified RMI endpoint is not a registry + */ @SuppressWarnings("unchecked") private void obtainBoundNames() throws java.rmi.NoSuchObjectException { @@ -55,6 +79,9 @@ private void obtainBoundNames() throws java.rmi.NoSuchObjectException allClasses.putAll(boundClasses.get(1)); } + /** + * Creates a method candidate from the specified signature on the command line. + */ private void createMethodCandidate() { String signature = (String)p.get("signature"); @@ -67,6 +94,15 @@ private void createMethodCandidate() } } + /** + * A RemoteObjectClient is used for communication to user registered RMI objects (anything other than + * registry, DGC or activator). This function returns a corresponding object that can be used for the + * communication. If an ObjID was specified on the command line, this ObjID is used as a target. Otherwise + * the client needs to be created for one particular bound name. + * + * @param boundName to create the client for. If ObjID was specified on the command line, it is preferred. + * @return RemoteObjectClient that can be used to communicate to the specified RMI object + */ private RemoteObjectClient getRemoteObjectClient(String boundName) { Object objID = p.get("objid"); @@ -86,6 +122,13 @@ private RemoteObjectClient getRemoteObjectClient(String boundName) } } + /** + * Is expected to be called other the method guessing. Takes a HashMap of bound name -> [MethodCandidaite] + * pairs and writes sample files for each bound name. The sample files contain Java code that can be used to + * call the corresponding remote methods. + * + * @param results HashMap of bound name -> [MethodCanidate] pairs + */ private void writeSamples(HashMap> results) { String templateFolder = (String)p.get("template-folder"); @@ -134,6 +177,11 @@ private void writeSamples(HashMap> results) Logger.decreaseIndent(); } + /** + * Parses the user specified wordlist options and creates a corresponding list of MethodCandidates. + * + * @return HashSet of MethodCandidates that should be used during guessing operations + */ private HashSet getCandidates() { HashSet candidates = new HashSet(); @@ -160,12 +208,19 @@ private HashSet getCandidates() return candidates; } + /** + * Dispatches the listen action. Basically just a handover to ysoserial. + */ @Parameters(count=4) public void dispatchListen() { YsoIntegration.createJRMPListener(p.getHost(), p.getPort(), p.getGadget()); } + /** + * Performs the gadgetCall operation on an ActivatorClient object. Used for deserialization + * attacks on the activator. + */ @Parameters(count=4) public void dispatchActivator() { @@ -173,25 +228,37 @@ public void dispatchActivator() act.gadgetCall(p.getGadget()); } + /** + * Performs the gadgetCall operation on a RegistryClient object. Used for deserialization + * attacks on the registry. + */ @Parameters(count=4) public void dispatchRegistry() { - String regMethod = p.validateRegMethod(); + String regMethod = p.getRegMethod(); boolean localhostBypass = (boolean)p.get("localhost-bypass"); RegistryClient reg = new RegistryClient(rmi); reg.gadgetCall(p.getGadget(), regMethod, localhostBypass); } + /** + * Performs the gadgetCall operation on a DGCClient object. Used for deserialization + * attacks on the DGC. + */ @Parameters(count=4) public void dispatchDGC() { - String dgcMethod = p.validateDgcMethod(); + String dgcMethod = p.getDgcMethod(); DGCClient dgc = new DGCClient(rmi); dgc.gadgetCall(dgcMethod, p.getGadget()); } + /** + * Performs the gadgetCall operation on a RemoteObjectClient object. Used for deserialization + * attacks on user registered RMI objects. Targets can be specified by bound name or ObjID. + */ @Parameters(count=3, requires= {"bound-name|objid","signature"}) public void dispatchMethod() { @@ -201,6 +268,10 @@ public void dispatchMethod() client.gadgetCall(candidate, p.getGadget(), argumentPosition); } + /** + * Performs the genericCall operation on a RemoteObjectClient object. Used for legitimate + * RMI calls on user registered RMI objects. Targets can be specified by bound name or ObjID. + */ @Parameters(count=3, requires= {"bound-name|objid","signature"}) public void dispatchCall() { @@ -210,6 +281,11 @@ public void dispatchCall() client.genericCall(candidate, argumentArray); } + /** + * Performs a codebase attack. The actual target is determined by the value of the --signature + * option. If the signature is a real method signature, a target needs to be specified by + * bound name or ObjID. Otherwise, the --signature is expected to be one of act, dgc or reg. + */ @SuppressWarnings("deprecation") @Parameters(count=4, requires= {"signature"}) public void dispatchCodebase() @@ -258,6 +334,10 @@ public void dispatchCodebase() } } + /** + * Performs the bind operation on the RegistryClient object. Binds the user specified gadget to + * the targeted registry. + */ @Parameters(count=4, requires= {"bound-name"}) public void dispatchBind() { @@ -267,6 +347,10 @@ public void dispatchBind() reg.bindObject(boundName, p.getGadget(), (boolean)p.get("localhost-bypass")); } + /** + * Performs the rebind operation on the RegistryClient object. Binds the user specified gadget to + * the targeted registry. + */ @Parameters(count=4, requires= {"bound-name"}) public void dispatchRebind() { @@ -276,6 +360,10 @@ public void dispatchRebind() reg.rebindObject(boundName, p.getGadget(), (boolean)p.get("localhost-bypass")); } + /** + * Performs the unbind operation on the RegistryClient object. Removes a bound name from the + * targeted registry endpoint. + */ @Parameters(requires= {"bound-name"}) public void dispatchUnbind() { @@ -285,13 +373,17 @@ public void dispatchUnbind() reg.unbindObject(boundName, (boolean)p.get("localhost-bypass")); } + /** + * Performs rmg's enumeration action. During this action, several different vulnerability types + * are enumerated. + */ public void dispatchEnum() { RMGUtils.enableCodebase(); RegistryClient registryClient = new RegistryClient(rmi); - String regMethod = p.validateRegMethod(); - String dgcMethod = p.validateDgcMethod(); + String regMethod = p.getRegMethod(); + String dgcMethod = p.getDgcMethod(); boolean localhostBypass = (boolean)p.get("localhost-bypass"); boolean enumJEP290Bypass = true; @@ -337,6 +429,11 @@ public void dispatchEnum() activationClient.enumActivator(); } + /** + * Performs a method guessing attack. During this operation, the specified wordlist files are parsed + * for valid method definitions and each method is invoked on the targeted RMI endpoint. Currently, this + * operation is only supported on registry endpoints and cannot be performed using the --objid option. + */ public void dispatchGuess() { boolean createSamples = (boolean)p.get("create-samples"); diff --git a/src/de/qtc/rmg/operations/MethodGuesser.java b/src/de/qtc/rmg/operations/MethodGuesser.java index fa8ea2e8..ba4ef68d 100644 --- a/src/de/qtc/rmg/operations/MethodGuesser.java +++ b/src/de/qtc/rmg/operations/MethodGuesser.java @@ -33,10 +33,7 @@ * * This allows to reliably detect remote methods without the risk of causing unwanted actions on the server * side by actually invoking them. The idea for such a guessing approach was not invented by remote-method-guesser, - * but was, to the best of our knowledge first implemented by the rmi-scout project. - * - * The MethodGuesser class was one of the first operation classes in rmg and is therefore not fully optimized - * to the currently available other utility classes. It may be restructured in future. + * but was, to the best of our knowledge, first implemented by the rmiscout project. * * @author Tobias Neitzel (@qtc_de) */ @@ -47,16 +44,17 @@ public class MethodGuesser { private RemoteObjectClient client; private HashSet candidates; + /** - * The MethodGuesser makes use of the official RMI API to obtain the RemoteObject from the RMI registry. - * Afterwards, it needs access to the underlying UnicastRemoteRef to perform customized RMi calls. Depending - * on the RMI version of the server (current proxy approach or legacy stub objects), this requires access to - * a different field within the Proxy or RemoteObject class. Both fields are made accessible within the constructor - * to make the actual guessing code more clean. + * The MethodGuesser relies on a RemoteObjectClient object to dispatch raw RMI calls. This object needs to be created + * in advance and has to be passed to the constructor. Other requirements are a HashSet of MethodCandidates to guess, + * the number of threads to use for guessing and a boolean indicating whether zero argument methods should be guessed. + * The problem with zero argument methods is, that they lead to real calls on the server side. * - * @param rmiRegistry registry to perform lookup operations - * @param unknownClasses list of unknown classes per bound name - * @param candidates list of method candidates to guess + * @param client RemoteObjectClient for the targeted RMI object + * @param candidates HashSet of MethodCandidates to guess + * @param threads number of threads to use + * @param zeroArg whether or not to guess zero argument methods */ public MethodGuesser(RemoteObjectClient client, HashSet candidates, int threads, boolean zeroArg) { @@ -67,6 +65,12 @@ public MethodGuesser(RemoteObjectClient client, HashSet candida this.zeroArg = zeroArg; } + /** + * Helper function that prints some visual text when the guesser is started. Just contains information + * on the number of methods that are guessed or the concrete method signature (if specified). + * + * @param candidates HashSet of MethodCandidates to guess + */ public static void printGuessingIntro(HashSet candidates) { int count = candidates.size(); @@ -89,40 +93,15 @@ public static void printGuessingIntro(HashSet candidates) } /** - * This lengthy function is used for method guessing. If targetName is not null, guessing is only performed on - * the specified bound name, otherwise all bound names within the registry are targeted. The function starts of - * with some initialization and then tries to determine the legacy status of the server. This status is required - * to decide whether to create the remote classes as interface or stub classes on the client side. Within legacy - * RMI, stub classes are required on the client side, but current RMI implementations only need an interface that - * is assigned to a Proxy. - * - * Depending on the determined legacy status, an interface or legacy stub class is now created dynamically. - * With the corresponding class now available on the class path, the RemoteObject can be looked up on the - * registry. From the obtained object, the RemoteRef is then extracted by using reflection. With this remote - * reference, customized RMI calls can now be dispatched. + * This method starts the actual guessing process. It iterates over the HashSet of MethodCandidate and creates a + * new GuessingWorker for each candidate. The corresponding workers are assigned to a thread pool and termination + * is awaited after all GuessingWorkers have been created. * - * This low level RMI access is required to call methods with invalid argument types. During method guessing, - * you want to call possibly existing remote methods with invalid argument types to prevent their actual execution. - * When using ordinary RMI to make a call, Java would refuse to use anything other than the expected argument types, - * as it would violate the interface or method definition. With low level RMI access, the call arguments can be - * manually written to the OutputStream which allows to use arbitrary arguments for a call. + * Each GuessingWorker obtains a reference to an ArrayList of MethodCandidates. Guessing workers are expected to + * push successfully guessed methods into this ArrayList. * - * To implement this confusion of argument types, the dynamically created classes for the RemoteObjects are - * constructed with two (interface)methods. The first, rmgInvokeObject, expects a String as first parameter, - * whereas the second, rmgInvokePrimitive, expects an int. Depending on the method signature that is currently - * guessed, rmgInvokeObject or rmgInvokePrimitive is used for the call. If the method signature expects a - * primitive as its first argument, rmgInvokeObject is used to cause the confusion. Otherwise, rmgInvokePrimitive - * will be used. During the call, the methodHash of rmgInvokeObject or rmgInvokePrimitive is replaced by the - * methodHash of the currently guessed method signature. This approach allows testing for arbitrary method signatures - * without manually exchanging the call parameters. - * - * @param targetName bound name to target. If null, target all available bound names - * @param threads number of threads to use for the operation - * @param zeroArg whether or not to also guess zero argument methods (are really invoked) - * @param legacyMode whether to enforce legacy stubs. 0 -> auto, 1 -> enforce legacy, 2 -> enforce normal - * @return List of successfully guessed methods per bound name + * @return ArrayList of successfully guessed methods */ - public ArrayList guessMethods() { Logger.printlnMixedYellow("Guessing methods on bound name:", client.getBoundName(), "..."); @@ -156,6 +135,14 @@ public ArrayList guessMethods() return existingMethods; } + /** + * The GuessingWorker class performs the actual method guessing in terms of RMI calls. It implements Runnable and + * is intended to be run within a thread pool. Each GuessingWorker has exactly one MethodCandidate assigned that is + * guessed by the worker. It uses the obtained RemoteObjectClient object to dispatch a call to that candidate and inspects + * the server-side exception to determine whether the method exists on the remote object. + * + * @author Tobias Neitzel (@qtc_de) + */ private class GuessingWorker implements Runnable { private MethodCandidate candidate; @@ -165,8 +152,9 @@ private class GuessingWorker implements Runnable { /** * Initialize the guessing worker with all the required information. * - * @param candidate method that is actually guessed - * @param existingMethods array of existing methods. Identified methods need to be appended + * @param client RemoteObjectClient to the targeted remote object + * @param candidate MethodCanidate to guess + * @param existingMethods ArrayList of existing methods. If candidate exists, it is pushed here */ public GuessingWorker(RemoteObjectClient client, MethodCandidate candidate, ArrayList existingMethods) { @@ -176,17 +164,13 @@ public GuessingWorker(RemoteObjectClient client, MethodCandidate candidate, Arra } /** - * Starts the method invocation. The RemoteObject that is used by this worker is actually a dummy - * object. It pretends to implement the remote class/interface, but actually has only two dummy methods - * defined. One of these dummy methods get invoked during this call, but with an exchanged method hash - * of the actual method in question. - * - * The selected method for the guessing expects either a primitive or non primitive type. This decision - * is made based on the MethodCandidate in question and with the goal of always causing a type confusion. - * E.g. when the MethodCandidate expects a primitive type as first argument, the method that expects a - * non primitive type is selected. Therefore, when the remote object attempts to unmarshal the call - * arguments, it will always find a type mismatch on the first argument, which causes an exception. - * This exception is used to identify valid methods. + * Invokes the assigned MethodCandidate. During the method invocation, a type-confusion mechanism is used. + * The assigned method is inspected for its first argument type and it is determined whether it is a primitive + * or non primitive type. Depending on the type, the exact opposite type is used for the actual method call. + * E.g. when the MethodCandidate expects a primitive type as first argument, we send a non primitive type, and + * the other way around. Therefore, when the remote object attempts to unmarshal the call arguments, it will always + * find a type mismatch on the first argument, which causes an exception. This exception is used to identify valid + * methods. */ public void run() { @@ -211,14 +195,14 @@ public void run() { } catch(java.rmi.UnmarshalException e) { /* - * This occurs on invocation of methods taking zero arguments. Since the call always succeeds, - * the remote method returns some value that probably not matches the expected return value of - * the lookup method. However, it is still a successful invocation and the method exists. + * This is basically a legacy catch. In the old way of invoking remote methods, this exception was + * thrown on existing methods that return a different argument type than expected. This should no longer + * occur, as arguments returned by the server are ignored by rmg. Nonetheless, the keep it for now. */ } catch(java.rmi.MarshalException e) { /* - * This one is may thrown on the client side why marshalling the call arguments. It should actually + * This one is thrown on the client side when marshalling the call arguments. It should actually * never occur and if it does, it is probably an internal error. */ StringWriter writer = new StringWriter(); @@ -251,8 +235,8 @@ public void run() { /* * Successfully guessed methods cause either an EOFException (object passed instead of primitive - * or two few arguments) or an OptionalDataException (primitive passed for instead of object). As - * these exceptions are not caught, we end up here. + * or two few arguments) or an OptionalDataException (primitive passed for instead of object) that + * is wrapped in a ServerException. As these exceptions caught, but not return, we end up here. */ Logger.printlnMixedYellow("HIT! Method with signature", candidate.getSignature(), "exists!"); existingMethods.add(candidate); diff --git a/src/de/qtc/rmg/operations/Operation.java b/src/de/qtc/rmg/operations/Operation.java index c8f67c89..0d81580d 100644 --- a/src/de/qtc/rmg/operations/Operation.java +++ b/src/de/qtc/rmg/operations/Operation.java @@ -4,6 +4,19 @@ import de.qtc.rmg.internal.ExceptionHandler; +/** + * The Operation enum class contains one item for each possible rmg action. An enum item consists out of + * the corresponding method name, the expected positional parameters and the helpstring that should be + * displayed for the method. This allows to keep all this information structured in one place without having + * to maintain it elsewhere. During the construction, the constructor of the Operation class looks up the specified + * method within the Dispatcher class and saves a reference to it. Methods are then invoked by using the + * Operation.invoke function. + * + * To create a new rmg action, a new enum item has to be created and the corresponding method has to be added to + * the Dispatcher class. + * + * @author Tobias Neitzel (@qtc_de) + */ public enum Operation { ACT("dispatchActivator", " ", "Performs Activator based deserialization attacks"), BIND("dispatchBind", "[gadget] ", "Binds an object to the registry thats points to listener"), @@ -22,6 +35,15 @@ public enum Operation { private String arguments; private String description; + /** + * Creates a new Operation item. The first argument (methodName) is used for a reflective method + * accesses within the Dispatcher class. The corresponding method is saved within the corresponding + * enum item. + * + * @param methodName name of the method that belongs to the operation + * @param arguments expected positional arguments + * @param description description of the method to show in the help menu + */ Operation(String methodName, String arguments, String description) { try { @@ -49,6 +71,11 @@ public String getArgs() return this.arguments; } + /** + * Invokes the method that was saved within the Operation. + * + * @param dispatcherObject object to invoke the method on + */ public void invoke(Dispatcher dispatcherObject) { try { @@ -58,6 +85,13 @@ public void invoke(Dispatcher dispatcherObject) } } + /** + * Iterates over the Operation enumeration and returns the operation that equals the specified + * operation name. + * + * @param name desired Operation to return + * @return requested Operation object or null if not found + */ public static Operation getByName(String name) { Operation returnItem = null; diff --git a/src/de/qtc/rmg/operations/RegistryClient.java b/src/de/qtc/rmg/operations/RegistryClient.java index 057a8ac6..fd60e30f 100644 --- a/src/de/qtc/rmg/operations/RegistryClient.java +++ b/src/de/qtc/rmg/operations/RegistryClient.java @@ -53,14 +53,17 @@ public RegistryClient(RMIWhisperer rmiEndpoint) /** * Invokes the bind method on the RMI endpoint. The used bound name can be specified by the user, - * but the bound RemoteObject is always an instance of RMIServerImpl_Stub. This is the default class + * and the bound RemoteObject is an instance of RMIServerImpl_Stub by default. This is a class * that is used by JMX and should therefore be available on most RMI servers. Furthermore, JMX is * a common RMI technology and binding this stub is probably most useful. The bind operation can also * be performed using CVE-2019-268, which may allows bind access from remote hosts. * + * The payload object (RMIServerImpl_Stub by default) is not created by the method itself but taken + * from the arguments. Users can use rmg's PluginSystem to bind different objects. The payload object + * is generated by a class that implements the IPayloadProvider interface. + * * @param boundName the bound name that will be bound on the registry - * @param host the host that is referenced by the bound RemoteObject. Clients will connect here - * @param port the port that is referenced by the bound RemoteObejct. Clients will connect here + * @param payloadObject the remote object to bind to the registry * @param localhostBypass whether to use CVE-2019-268 for the bind operation */ public void bindObject(String boundName, Object payloadObject, boolean localhostBypass) @@ -110,15 +113,11 @@ public void bindObject(String boundName, Object payloadObject, boolean localhost } /** - * Invokes the rebind method on the RMI endpoint. The used bound name can be specified by the user, - * but the bound RemoteObject is always an instance of RMIServerImpl_Stub. This is the default class - * that is used by JMX and should therefore be available on most RMI servers. Furthermore, JMX is - * a common RMI technology and binding this stub is probably most useful. The rebind operation can also - * be performed using CVE-2019-268, which may allows bind access from remote hosts. + * Invokes the rebind method on the RMI endpoint. Basically the same as the bind method that + * was already described above. * * @param boundName the bound name that will be rebound on the registry - * @param host the host that is referenced by the rebound RemoteObject. Clients will connect here - * @param port the port that is referenced by the rebound RemoteObejct. Clients will connect here + * @param payloadObject the remote object that is bind to the registry * @param localhostBypass whether to use CVE-2019-268 for the rebind operation */ public void rebindObject(String boundName, Object payloadObject, boolean localhostBypass) @@ -617,9 +616,9 @@ private void registryCall(String callName, MethodArguments callArguments, boolea } /** - * Helper function that maps call names to callIDs. The legacy RMI calling convention (that is used by default for registry - * operations) requires method calls to be made using an interfaceHash and callIDs. This function can be used to obtain the - * correct callID for the specified method. + * Helper function that maps call names to callIDs. The legacy RMI calling convention (that is used by default for + * registry operations) requires method calls to be made using an interfaceHash and callIDs. This function can be + * used to obtain the correct callID for the specified method. * * @param callName registry method to obtain the callID for * @return callID of the specified registry method @@ -646,7 +645,7 @@ private int getCallByName(String callName) /** * When calling registry methods with the new RMI calling convention, a method hash is required for each remote method. - * This function maps call names to their correspondign method hash. + * This function maps call names to their corresponding method hash. * * @param callName registry method to obtain the methodHash for * @return methodHash of the specified registry method @@ -678,7 +677,7 @@ private long getHashByName(String callName) * * @param callName registry method to generate the argument array for * @param payloadObject payload object to include into the argument array - * @return argument array that can be used for the specified registry call + * @return MethodArguments that can be used for the specified registry call */ private MethodArguments packArgsByName(String callName, Object payloadObject) { diff --git a/src/de/qtc/rmg/operations/RemoteObjectClient.java b/src/de/qtc/rmg/operations/RemoteObjectClient.java index d37daf41..7e0b057f 100644 --- a/src/de/qtc/rmg/operations/RemoteObjectClient.java +++ b/src/de/qtc/rmg/operations/RemoteObjectClient.java @@ -20,13 +20,9 @@ import javassist.NotFoundException; /** - * The method attacker is used to invoke RMI methods on the application level with user controlled - * objects as method arguments. It can be used to attempt codebase and deserialization attacks on - * known remote methods. Usually, you use first the *guess* operation to enumerate remote methods and - * then you use the *method* operation to check them for codebase and deserialization vulnerabilities. - * - * The MethodAttacker was one of the first operation classes in rmg and is therefore not fully optimized - * to the currently available other utility classes. It may be restructured in future. + * The RemoteObjectClient class is used for method guessing and communication to user defined remote objects. + * It can be used to perform regular RMI calls to objects specified by either a bound name or an ObjID. + * Apart from regular RMI calls, it also supports invoking methods with payload objects and user specified codebases. * * @author Tobias Neitzel (@qtc_de) */ @@ -53,8 +49,9 @@ public class RemoteObjectClient { * to make the actual attacking code more clean. * * @param rmiRegistry registry to perform lookup operations - * @param classes list of unknown classes per bound name - * @param targetMethod the remote method to target + * @param boundName for the lookup on the registry + * @param remoteClass class name of the bound name (is created dynamically to prevent ClassNotFoundExceptions) + * @param legacyMode the user specified legacyMode setting */ public RemoteObjectClient(RMIWhisperer rmiRegistry, String boundName, String remoteClass, int legacyMode) { @@ -77,6 +74,15 @@ public RemoteObjectClient(RMIWhisperer rmiRegistry, String boundName, String rem remoteRef = getRemoteRef(); } + /** + * When the ObjID of a remote object is already known, we can talk to this object without a previous lookup + * operation. In this case, the corresponding remote reference is constructed from scratch, as the ObjID and + * the target address (host:port) are the only required informations. + * + * @param rmiRegistry target where the object is located + * @param objID ID of the remote object to talk to + * @param legacyMode user specified legacyMode setting + */ public RemoteObjectClient(RMIWhisperer rmiRegistry, int objID, int legacyMode) { this.rmi = rmiRegistry; @@ -92,32 +98,15 @@ public String getBoundName() } /** - * This lengthy method performs the actual method call. If no bound name was specified, it iterates - * over all available bound names on the registry. After some initialization, the function checks the - * specified MethodCandidate for non primitive arguments and determines whether the remote endpoint - * uses legacy stubs. Non primitive arguments are required for codebase and deserialization attacks, - * whereas the legacy status of the server is required to decide whether to create the remote classes - * as interface or stub classes on the client side. Within legacy RMI, stub classes are required on the - * client side, but current RMI implementations only need an interface that is assigned to a Proxy. - * - * Depending on the determined legacy status, an interface or legacy stub class is now created dynamically. - * With the corresponding class now available on the class path, the RemoteObject can be looked up on the - * registry. From the obtained object, the RemoteRef is then extracted by using reflection. With this remote - * reference, a customized RMI call can now be dispatched. + * Invokes the specified MethodCandiate with a user specified payload object. This is used during deserialization + * attacks and needs to target non primitive input arguments of RMI methods. By default, the function attempts + * to find a non primitive method argument on it's own. However, by using the argumentPosition parameter, it is + * also possible to specify it manually. * - * This low level RMI access is required to call methods with invalid argument types. During deserialization - * attacks you may want to call a method that expects a HashMap with some other serialized object. When using - * ordinary RMI to make the call, Java would refuse to use anything other than a HashMap during the call, as - * it would violate the interface definition. With low level RMI access, the call arguments can be manually - * written to the stream which allows to use arbitrary arguments for the call. - * - * @param gadget object to use during the RMI call. Usually a payload object created by ysoserial - * @param boundName optional bound name to target. If null, target all bound names - * @param argumentPosition specify the argument position to attack. If negative, automatically search for non primitive - * @param operationMode the function was upgraded to support two operations 'codebase' or 'attack' - * @param legacyMode whether to enforce legacy stubs. 0 -> auto, 1 -> enforce legacy, 2 -> enforce normal + * @param targetMethod method to target for the attack + * @param gadget payload object to use for the call + * @param argumentPosition argument position to attack. Can be negative for auto selection. */ - public void gadgetCall(MethodCandidate targetMethod, Object gadget, int argumentPosition) { int attackArgument = findNonPrimitiveArgument(targetMethod, argumentPosition); @@ -182,6 +171,16 @@ public void gadgetCall(MethodCandidate targetMethod, Object gadget, int argument } } + /** + * This function invokes the specified MethodCandidate with a user specified codebase. The specified payload object + * is expected to be an instance of the class that should be loaded from the codebase. Usually this is created + * dynamically by rmg and the user has only to specify the class name. The function needs to target a non primitive + * method argument, that is selected by default, but users can also specify an argumentPosition explicitly. + * + * @param targetMethod method to target for the attack + * @param gadget instance of class that should be loaded from the client specified codebase + * @param argumentPosition argument to use for the attack. Can be negative for auto selection + */ public void codebaseCall(MethodCandidate targetMethod, Object gadget, int argumentPosition) { int attackArgument = findNonPrimitiveArgument(targetMethod, argumentPosition); @@ -246,6 +245,15 @@ else if( exceptionMessage.contains(randomClassName) ) { } } + /** + * This function is used for regular RMI calls on the specified MethodCandidate. It takes an array of Objects as + * input arguments and invokes the MethodCandidate with them accordingly. The function itself is basically just a + * wrapper around the genericCall function from the RMIWhisperer class. Especially the transformation from the raw + * Object array into the MethodArguments type is one of it's purposes. + * + * @param targetMethod remote method to call + * @param argumentArray method arguments to use for the call + */ public void genericCall(MethodCandidate targetMethod, Object[] argumentArray) { CtClass rtype = null; @@ -266,11 +274,27 @@ public void genericCall(MethodCandidate targetMethod, Object[] argumentArray) } } + /** + * Just a wrapper around the genericCall function from the RMIWhisperer class. Invokes the specified MethodCandidate + * with the specified set of MethodArguments. + * + * @param targetMethod method to invoke + * @param argumentArray arguments to use for the call + * @throws Exception this function is used e.g. for remote method guessing and raising all kind of exceptions is + * required. + */ public void rawCallNoReturn(MethodCandidate targetMethod, MethodArguments argumentArray) throws Exception { rmi.genericCall(null, -1, targetMethod.getHash(), argumentArray, false, getMethodName(targetMethod), remoteRef, null); } + /** + * Helper function that is used during deserialization and codebase attacks. It prints information on the selected + * argument position for the attack and also displays the parsed method signature again. + * + * @param targetMethod MethodCandidate that is attacked + * @param attackArgument the argument position of the argument that is attacked + */ private void printIntro(MethodCandidate targetMethod, int attackArgument) { Logger.printMixedBlue("Using non primitive argument type", targetMethod.getArgumentTypeName(attackArgument)); @@ -280,6 +304,12 @@ private void printIntro(MethodCandidate targetMethod, int attackArgument) Logger.println(""); } + /** + * Obtains a remote reference to the desired remote object. If this.objID is not null, the remote reference is always + * constructed manually by using the ObjID value. Otherwise, an RMI lookup is used to obtain it by bound name. + * + * @return Remote reference to the targeted object + */ private RemoteRef getRemoteRef() { if(this.objID == null) @@ -288,6 +318,13 @@ private RemoteRef getRemoteRef() return this.rmi.getRemoteRef(this.objID); } + /** + * This function obtains a remote reference by using the regular way. It looks up the bound name that was specified + * during construction of the RemoteObjectClient to obtain the corresponding object from the registry. Reflection + * is then used to make the remote reference accessible. + * + * @return Remote reference to the targeted object + */ private RemoteRef getRemoteRefByName() { boolean isLegacy = RMGUtils.isLegacy(this.remoteClass, this.legacyMode, true); @@ -321,6 +358,15 @@ private RemoteRef getRemoteRefByName() return remoteReference; } + /** + * Helper function to find the first non primitive argument within a method or to check whether the + * user specified argument position is really a non primitive. Basically relies on getPrimitive from + * the MethodCandidate class. The function itself is mainly concerned on the error handling. + * + * @param targetMethod MethodCandidate to look for non primitives + * @param position user specified argument position + * @return position of the first non primitive argument or the user specified position if primitive + */ private int findNonPrimitiveArgument(MethodCandidate targetMethod, int position) { int attackArgument = 0; @@ -343,6 +389,20 @@ private int findNonPrimitiveArgument(MethodCandidate targetMethod, int position) return attackArgument; } + /** + * During deserialization and codebase attacks, rmg uses a canary to check whether the attack was successful. + * Instead of sending the plain payload object to the RMI endpoint, rmg always sends an Object array that consists + * out of the actual payload Object and a canary class. The canary class is randomly generated during runtime and + * passed in the second position within the Object array. The payload itself is used in the first position. + * + * Only if the payload object was successfully processed on the RMI server, it will attempt to load the canary class, + * that leads to a ClassNotFoundException. This makes it reliably detectable whether an attack was successful. + * + * @param targetMethod MethodCandidate to create the payload for. + * @param gadget payload object to use in the payload + * @param attackArgument position of a non primitive argument + * @return MethodArguments to use for the call + */ @SuppressWarnings("deprecation") private MethodArguments prepareArgumentArray(MethodCandidate targetMethod, Object gadget, int attackArgument) { @@ -381,6 +441,14 @@ private MethodArguments prepareArgumentArray(MethodCandidate targetMethod, Objec return callArguments; } + /** + * Due to other internal requirements, the getName function from the MethodCandidate class does not + * implement exception handling. Therefore, this function provides a simple wrapper class that catches + * exceptions. + * + * @param targetMethod MethodCandidate to obtain the name from + * @return method name of the MethodCandidate + */ private String getMethodName(MethodCandidate targetMethod) { String methodName = ""; diff --git a/src/de/qtc/rmg/plugin/DefaultProvider.java b/src/de/qtc/rmg/plugin/DefaultProvider.java index c7177e93..b5b7ed10 100644 --- a/src/de/qtc/rmg/plugin/DefaultProvider.java +++ b/src/de/qtc/rmg/plugin/DefaultProvider.java @@ -14,8 +14,30 @@ import javassist.CtMethod; import javassist.CtNewMethod; +/** + * The DefaultProvider is a default implementation of an rmg plugin. It implements the IArgumentProvider + * and IPayloadProvider and is always loaded when no user specified plugin overwrites one of these interfaces. + * + * Within its IPayloadProvider override, it returns either a RMIServerImpl object as used by JMX (for bind, rebind + * and unbin actions) or a ysoserial gadget (for basically all other actions). The IArgumentProvider override attempts + * to evaluate the user specified argument string as Java code and attempts to create an Object array out of it that + * is used for method calls. + * + * @author Tobias Neitzel (@qtc_de) + */ public class DefaultProvider implements IArgumentProvider, IPayloadProvider { + /** + * Return an RMIServerImpl object as used by JMX endpoints when invoked from the bind, rebind or unbind + * actions. In this case, name is expected to be 'jmx' or args is expected to be null. When the name is + * 'jmx', the args parameter is expected to contain the address definition for the remote object (host:port). + * Otherwise, if args is null and the name is not 'jmx', name is expected to contain the listener definition. + * This allows to perform the bind like 'rmg 127.0.0.1 9010 bind jmx 127.0.0.1:4444' or like + * 'rmg 127.0.0.1 9010 bind 127.0.0.1:4444'. + * + * Otherwise, pass the user specified gadget name and gadget arguments to ysoserial and return the + * corresponding gadget. + */ @Override public Object getPayloadObject(Operation action, String name, String args) { @@ -53,6 +75,13 @@ public Object getPayloadObject(Operation action, String name, String args) return null; } + /** + * This function performs basically an eval operation on the user specified argumentString. The argument string is + * inserted into the following expression: return new Object[] { " + argumentString + "}; + * This expression is evaluated and the resulting Object array is returned by this function. For this to work it is + * important that all arguments within the argumentString are valid Java Object definitions. E.g. one has to use + * new Integer(5) instead of a plain 5. + */ @Override public Object[] getArgumentArray(String argumentString) { diff --git a/src/de/qtc/rmg/plugin/IArgumentProvider.java b/src/de/qtc/rmg/plugin/IArgumentProvider.java index 46226046..3e996d09 100644 --- a/src/de/qtc/rmg/plugin/IArgumentProvider.java +++ b/src/de/qtc/rmg/plugin/IArgumentProvider.java @@ -1,5 +1,15 @@ package de.qtc.rmg.plugin; +/** + * The IArgumentProvider interface is used during rmg's 'call' action to obtain the argument array that should be + * used for the call. plugins can implement this class to obtain custom argument arrays that they want to use during + * the 'call' operation. The getArgumentArray method is called with the user specified argument string and is expected + * to return the Object array that should be used for the call. + * + * This interface is implemented by rmg's DefaultProvider class by default. + * + * @author Tobias Neitzel (@qtc_de) + */ public interface IArgumentProvider { Object[] getArgumentArray(String argumentString); } diff --git a/src/de/qtc/rmg/plugin/IPayloadProvider.java b/src/de/qtc/rmg/plugin/IPayloadProvider.java index 5b4c98ff..d7790448 100644 --- a/src/de/qtc/rmg/plugin/IPayloadProvider.java +++ b/src/de/qtc/rmg/plugin/IPayloadProvider.java @@ -2,6 +2,17 @@ import de.qtc.rmg.operations.Operation; +/** + * The IPayloadProvider interface is used during all rmg actions that send payload objects to the remote server. + * This includes all actions that perform desrialization attacks, but also the bind, rebind and unbind actions. + * Implementors are expected to implement the getPayloadObject function, that is called to obtain the actual payload + * object. The function takes the current rmg action (in case you want to provide different gadgets for different calls) + * and the gadget name and gadget arguments that were specified on the command line. + * + * This interface is implemented by rmg's DefaultProvider class by default. + * + * @author Tobias Neitzel (@qtc_de) + */ public interface IPayloadProvider { Object getPayloadObject(Operation action, String name, String args); } diff --git a/src/de/qtc/rmg/plugin/IResponseHandler.java b/src/de/qtc/rmg/plugin/IResponseHandler.java index a37bda83..a652dd31 100644 --- a/src/de/qtc/rmg/plugin/IResponseHandler.java +++ b/src/de/qtc/rmg/plugin/IResponseHandler.java @@ -1,5 +1,14 @@ package de.qtc.rmg.plugin; +/** + * The IResponseHandler interface is used during rmg's 'call' action to handle the return value of an invoked method. + * Implementors are expected to implement the handleResponse method that is called with the return object obtained by the + * server. + * + * This interface is not implemented by default and server responses are ignored when no plugin was specified manually. + * + * @author Tobias Neitzel (@qtc_de) + */ public interface IResponseHandler { - void handleResponse(Object responseObject); + void handleResponse(Object responseObject); } diff --git a/src/de/qtc/rmg/plugin/PluginSystem.java b/src/de/qtc/rmg/plugin/PluginSystem.java index 440e84f1..e0475ae7 100644 --- a/src/de/qtc/rmg/plugin/PluginSystem.java +++ b/src/de/qtc/rmg/plugin/PluginSystem.java @@ -13,6 +13,17 @@ import de.qtc.rmg.operations.Operation; import de.qtc.rmg.utils.RMGUtils; +/** + * The PluginSystem class allows rmg to be extended by user defined classes. It can be used to setup + * payload and argument providers that are used to create call arguments and to setup response handlers + * that process return values of RMI calls. Plugins can be loaded by using the --plugin option on the + * command line. + * + * By default, rmg uses the DefaultProvider as plugin, which implements the IPayloadProvider and + * IArgumentProvider interfaces. + * + * @author Tobias Neitzel (@qtc_de) + */ public class PluginSystem { private static String manifestAttribute = "RmgPluginClass"; @@ -21,6 +32,13 @@ public class PluginSystem { private static IResponseHandler responseHandler = null; private static IArgumentProvider argumentProvider = null; + /** + * Initializes the plugin system. By default, the payloadProvider and argumentProvider get a DefaultProvider + * instance assigned. The responseHandler is not initialized by default and stays at null. If a user specified + * pluginPath was specified, the plugin is attempted to be loaded and may overwrite previous settings. + * + * @param pluginPath user specified plugin path or null + */ public static void init(String pluginPath) { DefaultProvider provider = new DefaultProvider(); @@ -31,6 +49,17 @@ public static void init(String pluginPath) loadPlugin(pluginPath); } + /** + * Attempts to load the plugin from the user specified plugin path. Plugins are expected to be JAR files that + * contain the 'RmgPluginClass' attribute within their manifest. The corresponding attribute needs to contain the + * class name of the class that actually implements the plugin. + * + * rmg will attempt to load the specified class using an URLClassLoader. It then attempts to identify which interfaces + * are implemented by the class. E.g. if the class implements the IPayloadProvider interface, the default + * payloadProvider of the PluginSystem class gets overwritten with the class from the plugin. + * + * @param pluginPath file system path to the plugin to load + */ @SuppressWarnings("deprecation") private static void loadPlugin(String pluginPath) { @@ -91,31 +120,67 @@ private static void loadPlugin(String pluginPath) } } + /** + * Is called on incoming server responses if a response handler is defined. Just forwards the call to the + * responseHandler plugin. + * + * @param o return value of a RMI method call + */ public static void handleResponse(Object o) { responseHandler.handleResponse(o); } + /** + * Is called from each action that requires a payload object. Just forwards the call to the corresponding plugin. + * + * @param action action that requests the payload object + * @param name name of the payload that is requested + * @param args arguments that should be used for the payload + * @return generated payload object + */ public static Object getPayloadObject(Operation action, String name, String args) { return payloadProvider.getPayloadObject(action, name, args); } + /** + * Is called during rmg's 'call' action to obtain the Object argument array. Just forwards the call to the corresponding + * plugin. + * + * @param argumentString as specified on the command line + * @return Object array to use for the call + */ public static Object[] getArgumentArray(String argumentString) { return argumentProvider.getArgumentArray(argumentString); } + /** + * Checks whether a responseHandler was registered. + * + * @return true or false + */ public static boolean hasResponseHandler() { return responseHandler instanceof IResponseHandler; } + /** + * Checks whether a payloadProvider was registered. + * + * @return true or false + */ public static boolean hasPayloadProvider() { return payloadProvider instanceof IPayloadProvider; } + /** + * Checks whether a argumentProvider was registered. + * + * @return true or false + */ public static boolean hasArgumentProvider() { return argumentProvider instanceof IArgumentProvider; diff --git a/src/de/qtc/rmg/utils/RMGUtils.java b/src/de/qtc/rmg/utils/RMGUtils.java index ed2d43b5..68676000 100644 --- a/src/de/qtc/rmg/utils/RMGUtils.java +++ b/src/de/qtc/rmg/utils/RMGUtils.java @@ -1,8 +1,5 @@ package de.qtc.rmg.utils; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.rmi.Remote; @@ -10,10 +7,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Properties; import java.util.UUID; -import de.qtc.rmg.Starter; import de.qtc.rmg.internal.ExceptionHandler; import de.qtc.rmg.internal.MethodArguments; import de.qtc.rmg.internal.MethodCandidate; @@ -551,7 +546,7 @@ public static String getCast(CtClass type) } /** - * Just a wrapper around Syste.exit(1) that prints an information before quitting. + * Just a wrapper around System.exit(1) that prints an information before quitting. */ public static void exit() { @@ -559,60 +554,6 @@ public static void exit() System.exit(1); } - /** - * Loads the remote-method-guesser configuration file from the specified destination. This function may be used - * twice during the startup of rmg. First it is used to load the default configuration, which is done via - * getResourceAsStream. In this case extern should be set to false and the prop argument is an empty Properties - * object. - * - * Afterwards, the function may be called again with a user defined configuration file. In this case, extern should - * be set to true and the prop arguments should contain a Properties object that already contains the default - * configuration. - * - * @param filename file system path to load the configuration file from - * @param prop a Properties object to store the parsed properties - * @param extern whether to use FileInputStream (true) or getResourceAsStream (false) to read the properties file - */ - public static void loadConfig(String filename, Properties prop, boolean extern) - { - if(filename == null) - return; - - try { - - InputStream configStream = null; - - if( extern ) - configStream = new FileInputStream(filename); - else - configStream = Starter.class.getResourceAsStream(filename); - - prop.load(configStream); - configStream.close(); - - } catch( IOException e ) { - ExceptionHandler.unexpectedException(e, "loading", ".properties file", true); - } - } - - /** - * Small helper function that checks whether a HashMap contains items and returns an error message that fits - * for the desired situation. - * - * @param unknownClasses HashMap to investigate - * @return true if the HashMap contains items, false otherwise - */ - public static boolean containsObjects(HashMap availableClasses) - { - if( availableClasses.size() <= 0 ) { - Logger.eprintln("There are no remote objects present on the registry."); - Logger.eprintln("Guessing methods not necessary."); - return false; - } - - return true; - } - /** * Sets the useCodebaseOnly setting to false and configures the CodebaseCollector class as the RMIClassLoaderSpi. * This is required to get access to server side exposed codebases, which is one of the things that rmg reports during @@ -704,9 +645,14 @@ private static void addSerialVersionUID(CtClass ctClass) throws CannotCompileExc /** * Helper method that adds remote methods present on known remote objects to the list of successfully guessed methods. - * - * @param knownClasses list of boundName - className pairs of known classes - * @param guessedMethods list of successfully guessed methods + * The known remote object classes are looked by by using the CtClassPool. Afterwards, all implemented interfaces + * of the corresponding CtClass are iterated and it is checked whether the interface extends java.rmiRemote (this + * is required for all methods, that can be called from remote). From these interface,s all methods are obtained + * and added to the list of successfully guessed methods. + * + * @param boundName bound name that is using the known class + * @param className name of the class implemented by the bound name + * @param guessedMethods list of successfully guessed methods (bound name -> list) */ public static void addKnownMethods(String boundName, String className, HashMap> guessedMethods) { @@ -764,6 +710,22 @@ public static String getSimpleSignature(CtMethod method) return simpleSignature.toString(); } + /** + * During regular RMI calls, method arguments are usually passed as Object array as methods are invoked using a + * Proxy mechanism. However, on the network layer argument types need to be marshalled according to the expected + * type from the method signature. E.g. an argument value might be an Integer, but is epxected by the method as int. + * Therefore, passing an Object array alone is not sufficient to correctly write the method arguments to the output + * stream. + * + * This function takes the remote method that is going to be invoked and an Object array of parameters to use for + * the call. It then creates a MethodArguments object, that contains Pairs that store the desired object value + * together with their corresponding type that is expected by the remote method. + * + * @param method CtMethod that is going to be invoked + * @param parameterArray array of arguments to use for the call + * @return MerhodArguments - basically a list of Object value -> Type pairs + * @throws NotFoundException + */ public static MethodArguments applyParameterTypes(CtMethod method, Object[] parameterArray) throws NotFoundException { CtClass type; @@ -803,6 +765,13 @@ public static MethodArguments applyParameterTypes(CtMethod method, Object[] para return parameterMap; } + /** + * Helper function that is called to split a string that contains a listener definition (host:port). + * The main benefit of this function is, that it implements basic error handling. + * + * @param listener listener definition as string + * @return split listener [host, port] + */ public static String[] splitListener(String listener) { String[] split = listener.split(":"); @@ -814,6 +783,13 @@ public static String[] splitListener(String listener) return split; } + /** + * Enables a user specified codebase within the MaliciousOutputStream. If the user specified address does not start + * with a protocol definition, 'http' is prefixed by default. Furthermore, if no typical java extension was specified, + * a slash is added to the end of the URL. + * + * @param serverAddress user specified codebase address. + */ public static void setCodebase(String serverAddress) { if( !serverAddress.matches("^(https?|ftp|file)://.*$") ) @@ -831,9 +807,13 @@ public static void setCodebase(String serverAddress) * it is fine to include the code in a GPLv3 licensed project and to convey the license to GPLv3. * (https://www.gnu.org/licenses/gpl-faq.en.html#AllCompatibility) * - * @param thisCtClass - * @param targetClassName - * @return + * The code is used to implement isAssignableFrom for CtClasses. It checks whether thisCtClass is the same as, + * extends or implements targetClassName. Or in other words: It checks whether targetClassName is the same as, + * or is a superclass or superinterface of the class or interface represented by the thisCtClass parameter. + * + * @param thisCtClass class in question + * @param targetClassName name of the class to compare against + * @return true if targetClassName is the same as, or is a superclass or superinterface of thisCtClass */ public static boolean isAssignableFrom(CtClass thisCtClass, String targetClassName) { diff --git a/src/de/qtc/rmg/utils/YsoIntegration.java b/src/de/qtc/rmg/utils/YsoIntegration.java index 0b3eec81..6554664d 100644 --- a/src/de/qtc/rmg/utils/YsoIntegration.java +++ b/src/de/qtc/rmg/utils/YsoIntegration.java @@ -51,6 +51,14 @@ public class YsoIntegration { private static String ysoPath; private static String[] bypassGadgets = new String[]{"JRMPClient2", "AnTrinh"}; + /** + * Basically a wrapper around prapreAnTrinhGadget that attempts to parse the command string as a listener + * before creating the listener object. This is called during gadget creation, when the specified gadget + * is the AnTrinh gadget. + * + * @param command expected to be in listener format (host:port) + * @return AnTrinh bypass gadget + */ private static Object generateBypassGadget(String command) { Object payloadObject = null; @@ -73,7 +81,6 @@ private static Object generateBypassGadget(String command) * Just a small wrapper around the URLClassLoader creation. Checks the existence of the specified file * path before creating a class loader around it. * - * @param ysoPath file system path to the ysoserial .jar file * @return URLClassLoader for ysoserial classes * @throws MalformedURLException when the specified file system path exists, but is invalid */ @@ -127,11 +134,9 @@ private static InetAddress getLocalAddress(String host) * the listening host in this implementation. The JRMPListener will then only be opened on the specified * IP address. * - * @param ysoPath file system path to the ysoserial .jar file * @param host IP address where to listen for connections * @param port port where to listen for connections - * @param gadget ysoserial gadget name to send within responses - * @param command ysoserial gadget command to use for gadget generation + * @param payloadObject to deliver to incoming connections */ public static void createJRMPListener(String host, int port, Object payloadObject) { @@ -190,7 +195,6 @@ public static void createJRMPListener(String host, int port, Object payloadObjec * Loads ysoserial using and separate URLClassLoader and invokes the makePayloadObject function by using * reflection. The result is a ysoserial gadget as it would be created on the command line. * - * @param ysoPath file system path to ysoserial .jar file * @param gadget name of the desired gadget * @param command command specification for the desired gadget * @return ysoserial gadget