java manual remote execution

A demand scenario

When a bug occurs on the server, but because one or two logs are not printed, and the specific problem cannot be located in the end, it is very annoying. Remote execution can provide dynamic enhancement functions for the program without modifying the server program. , Print out the information you want.

2. In the process of program implementation, we need to solve the following 3 problems

·How to compile the Java code submitted to the server?
·How to execute the compiled Java code?
·How to collect the execution result of Java code?

The first problem is solved: you
need to implement an upload file interface for uploading the compiled class file to the server. This class file is the final executed file. This file can provide you with the service output capabilities you want.
The second problem is solved:
let the class loader load this class to generate a Class object, and then call a certain method in reflection.
The solution to the second problem:
here is the method of direct output to the log.

Three realizations

1.HotSwapClassLoader.java
public class HotSwapClassLoader extends ClassLoader{
    
    

    public HotSwapClassLoader(){
    
    
        super(HotSwapClassLoader.class.getClassLoader());
    }

    public Class loadByte(byte[] classByte){
    
    

        return defineClass(null,classByte,0,classByte.length);
    }
}

What HotSwapClassLoader does is just to expose the protected method
defineClass( ) in the parent class (ie java.lang.ClassLoader) . We will use this method to turn the byte[] array of the Java class submitted for execution into a Class object.
HotSwapClassLoader does not override the loadClass() or findClass() method, so if you don’t count the external manual call to loadByte()
method, the class search range of this class loader is exactly the same as its parent class loader. When the virtual machine is called, it will be
handed over to the parent class to load according to the parent delegation model. The constructor specifies the class loader that loads the HotSwapClassLoader class as the parent class
loader. This step is the key to realize that the submitted execution code can access the server-side reference class library.

2.HackSystem.java
public class HackSystem {
    
    
    public final static InputStream in = System.in;
    public static ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    public final static PrintStream out = new PrintStream(buffer);
    public final static PrintStream err = out;

    public static String getBufferString(){
    
    
        return buffer.toString();
    }

    public static void clearBuffer(){
    
    
        buffer.reset();
    }

    public static void setSecurityManager(final SecurityManager s){
    
    
        System.setSecurityManager(s);
    }

    public static SecurityManager getSecurityManager(){
    
    
        return System.getSecurityManager();
    }

    public static long currenttimeMillis(){
    
    
        return System.currentTimeMillis();
    }

    public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length){
    
    
        System.arraycopy(src,srcPos,dest,destPos,length);
    }

    public static int identityHashCode(Object x){
    
    
        return System.identityHashCode(x);
    }
}

The second class is to implement the process of replacing java.lang.System with the HackSystem class defined by ourselves. It directly modifies
the constant pool part in the byte[] array conforming to the Class file format, and changes the constant_Utf8_info constant of the specified content in the constant pool. Replace with
a new string, the specific code is shown in the following code listing [3.ClassModifier.java]. The part of ClassModifier involved in the operation of byte[] array is mainly to convert byte[] to int and String, and to encapsulate the replacement operation of byte[] data in the ByteUtils shown in the code listing [4.ByteUtils.java] in

3.ClassModifier.java
public class ClassModifier {
    
    


    private static final int CONSTANT_POOL_COUNT_INDEX = 8;


    private static final int CONSTANT_Utf8_info = 1;


    private static final int[]  CONSTANT_ITEM_LEGTH = {
    
    -1,-1,-1,5,5,9,9,3,3,5,5,5,5};

    private static final int u1 = 1;
    private static final int u2 = 2;

    private byte[] classByte;

    public ClassModifier(byte[] classByte){
    
    
        this.classByte = classByte;
    }


    public byte[] modifyUTF8Constant(String oldStr,String newStr){
    
    
        int cpc = getConstantPoolcount();
        int offset = CONSTANT_POOL_COUNT_INDEX + u2;
        for(int i =0;i <cpc;i++){
    
    
            int tag = ByteUtils.bytes2Int(classByte,offset,u1);
            if(tag ==  CONSTANT_Utf8_info){
    
    
                int len = ByteUtils.bytes2Int(classByte,offset+u1,u2);
                offset += (u1+u2);
                String str = ByteUtils.bytes2String(classByte,offset,len);
                if(str.equalsIgnoreCase(oldStr)){
    
    
                    byte[] strBytes = ByteUtils.string2Bytes(newStr);
                    byte[] strLen = ByteUtils.int2Bytes(newStr.length(),u2);
                    classByte = ByteUtils.bytesReplace(classByte,offset-u2,u2,strLen);
                    classByte = ByteUtils.bytesReplace(classByte,offset,len,strBytes);
                    return classByte;
                }else{
    
    
                    offset += len;
                }
            }else{
    
    
                offset += CONSTANT_ITEM_LEGTH[tag];
            }
        }
        return classByte;
    }

    public int getConstantPoolcount(){
    
    
        return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX,u2);
    }


}

The byte[] array processed by ClassModifier will be passed to the HotSwapClassLoader.loadByte() method for class loading.
After replacing the symbol reference with the byte[] array here, directly reference the HackSystem class in the Java code with the client and then compile the generated Class It's exactly the same. This implementation not only avoids the client's dependence on specific classes when writing temporary execution code (otherwise the
HackSystem cannot be introduced ), but also prevents the server from affecting the output of other programs after modifying the standard output.

4.ByteUtils.java
public class ByteUtils {
    
    

    public static int bytes2Int(byte[] b,int start,int len){
    
    
        int sum = 0;
        int end = start + len;
        for(int i =start;i<end;i++){
    
    
            int n = ((int)b[i]) & 0xff;
            n <<= (--len) * 8;
            sum = n+sum;
        }
        return sum;
    }

    public static byte[] int2Bytes(int value,int len){
    
    
        byte[] b = new byte[len];
        for(int i=0;i<len;i++){
    
    
            b[len-i-1] = (byte) ((value>> 8 * i) & 0xff);
        }
        return b;
    }

    public static String bytes2String(byte[] b,int start,int len){
    
    
        return new String(b,start,len);
    }

    public static byte[] string2Bytes(String str){
    
    
        return str.getBytes();
    }

    public static byte[] bytesReplace(byte[] originalBytes,int offset ,int len,byte[] replaceBytes){
    
    
        byte[] newBytes = new byte[originalBytes.length+(replaceBytes.length-len)];
        System.arraycopy(originalBytes,0,newBytes,0,offset);
        System.arraycopy(replaceBytes,0,newBytes,offset,replaceBytes.length);
        System.arraycopy(originalBytes,offset+len,newBytes,offset+replaceBytes.length,originalBytes.length-offset-len);
        return newBytes;

    }
}

4.JavaclassExecuter.java

The four support classes have been explained. Let's take a look at the last class, JavaclassExecuter, which is the entry point for external calls. The
assembly logic of the previous support classes is called to complete the class loading work. Only one JavaclassExecuter execute () method, input with
the in line with the Class file format byte [] array java.lang.System replaced symbolic references used to generate a load HotSwapClassLoader Class object, since each time execution execute () method will Generate a new class loader instance, so the same class can be loaded repeatedly. Then the main() method of this Class object is called by reflection. If any exception occurs during the period, the exception information is
printed to HackSystem.out, and finally the information in the buffer is returned as the result of the method. The implementation code of JavaclassExecuter is
as follows:

public class JavaclassExecuter {
    
    
    public static String execute(byte[] classByte){
    
    
        HackSystem.clearBuffer();
        ClassModifier cm = new ClassModifier(classByte);
        byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System","sgcc/supplier/service/front/shops/controller/remoteexecute/tmppak/HackSystem");
        HotSwapClassLoader loader = new HotSwapClassLoader();
        Class clazz = loader.loadByte(modiBytes);

        try {
    
    
            Method method = clazz.getMethod("main",new Class[]{
    
    String[].class});
            method.invoke(null,new String[]{
    
    null});
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return HackSystem.getBufferString();
    }
}

Four verification

Verification method one:

Write a Java class arbitrarily, the content does not matter, just output information to System.out, named TestClass, and put it in the root directory of the C drive of the server. Then create a JSP file and write the following content, you can see the running results of this class in the browser, this kind of verification requires the establishment of an ordinary web project, and put 4 support classes and a jsp code into In the project, the jsp code can be directly put into index.jsp. The above is used to test TestClass.java class information and jsp information.

public class TestClass{
    
    
    public static void main(String[] args){
    
    
       System.out.println("这是第一句。。。");
       System.out.println("这是第二句。。。");
       System.out.println("这是第三句。。。");
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="com.esgcc.*" %><%--这里传入的是4个支持类的路径--%>
<%
  InputStream is = new FileInputStream("C:/TestClass.class");
  byte[] b = new byte[is.available()];
  is.read(b);
  is.close();

  out.println("<textarea style='width:1000;height:800'>");
  out.println(JavaclassExecuter.execute(b));
  out.println("</textarea>");
%>

Start the project and you can see the page output:
Insert picture description here

Verification method two:

The first is to output the information to the page. We can also output the information directly to the console. Here we only need to create an arbitrary java project, and put the 4 support classes and the following code in as much as possible. The implementation code is actually the same:

public class MainEntrance {
    
    
    public MainEntrance() {
    
    
    }

    public static void main(String[] args) {
    
    
        try {
    
    
            InputStream is = new FileInputStream("c:/TestClass.class");
            byte[] b = new byte[is.available()];
            is.read(b);
            is.close();
            JavaclassExecuter.execute(b);
        } catch (IOException var3) {
    
    
            var3.printStackTrace();
        }

    }
}

Execute the main method above to get the output:
Insert picture description here

Verification method three:

Here is to use a new interface in SpringBoot, use the interface to trigger execution, upload the above 4 support class codes to the server, and upload the called TestClass.class to the opt directory. The interface implementation code is as follows:.
code show as below:

@Slf4j
@RestController
@RequestMapping("/remote")
public class RemoteExecuteController {
    
    


    /**
     * @param
     * @return
     */
    @PostMapping(value = "/execute")
    @ApiOperation(value = "远程", notes = "远程")
    @ApiImplicitParams({
    
    
            @ApiImplicitParam(name = "jsonParams", value = "参数集合: \n" +
                    "{\n" +
                    "}", required = true, dataTypeClass = String.class, paramType = "query"),
            @ApiImplicitParam(name = "userToken", value = "token", required = true, dataTypeClass = String.class, paramType = "header")
    })
    public String remoteExecute(String jsonParams) {
    
    

        try {
    
    

            log.info("::::::::::::::::::::::进入执行接口::::::::::::::::::::::");

            InputStream is = new FileInputStream("/opt/TestClass.class");
            byte[] b = new byte[is.available()];
            is.read(b);
            is.close();
            log.info("::::::::::::::::::::::远程执行日志打印开始::::::::::::::::::::::");
            log.info(JavaclassExecuter.execute(b));
            log.info("::::::::::::::::::::::远程执行日志打印结束::::::::::::::::::::::");
        } catch (IOException e) {
    
    
            e.printStackTrace();
            log.error(e.getMessage());
        }
        return "success";
    }

}

Check the log after calling the interface through postman, there is output:
Insert picture description here

Five summary

This is just a demonstration. It does not actually call the method you want to call in the TestClass class. It just provides a framework, hoping to inspire friends who see these codes.

Reference materials:

"In-depth understanding of the java virtual machine-3rd edition"

Guess you like

Origin blog.csdn.net/m0_46897923/article/details/112653799