如何在服务器上执行临时代码?


排查问题的过程中,很多时候需要在服务器中执行一小段程序代码,就可以定位或排除问题,但就是偏偏找不到可以让服务器执行临时代码的途径,让人恨不得在服务器上装个后门。通常解决类问题有以下几种途径:

  1. 可以使用BTrace这类JVMTI工具去动态修改程序中某一部分的运行代码,类似的JVMTI工具还有阿里巴巴的Arthas等。

  2. 使用JDK 6之后提供了Compiler API,可以动态地编译Java程序,这样虽然达不到动态语言的灵活度,但让服务器执行临时代码的需求是可以得到解决的。

  3. 也可以通过“曲线救国”的方式来做到,譬如写一个JSP文件上传到服务器,然后在浏览器中运行它,或者在服务端程序中加入一个BeanShell Script、JavaScript等的执行引擎(如Mozilla Rhino)去执行动态脚本。

  4. 在应用程序中内置动态执行的功能

目的:

在实现“在服务端执行临时代码”,不依赖某个JDK版本,不改变原有服务端程序的部署,不依赖任何第三方类库,不侵入原有程序,即无须改动原程序的任何代码。也不会对原有程序的运行带来任何影响

思路

在程序实现的过程中,我们需要解决以下3个问题:

  • 如何编译提交到服务器的Java代码?
  • 如何执行编译之后的Java代码?
  • 如何收集Java代码的执行结果?

第一个问题两种方案可以选择,一种在服务器上编译,在JDK 6以后可以使用Compiler API,在JDK 6以前可以使用tools.jar包(在JAVA_HOME/lib目录下)中的com.sun.tools.Javac.Main类来编译Java文件,它们其实和直接使用Javac命令来编译是一样的。再或者直接在本地编译。

第二个问题,要执行编译后的Java代码,让类加载器加载这个类生成一个Class对象,然后反射调用一下某个方法就可以了(可以直接使用main方法)。提交上去的类要能访问到服务端的其他类库才行。还有就是既然提交的是临时代码,那提交的Java类在执行完后就应当能被卸载和回收掉。

第三个问题,直接在执行的类中把对System.out的符号引用替换为我们准备的PrintStream的符号引用

实现

公开java.lang.ClassLoader中的protected方法defineClass()

package com.testpak.RemoteExcutor;

/**
 * 为了多次载入执行类而加入的加载器
 * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
 *  由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载
 */
public class HotSwapClassLoader extends ClassLoader {

    public HotSwapClassLoader() {
        super(HotSwapClassLoader.class.getClassLoader());
    }
    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }

}

创建ClassModifier提供修改常量池常量的功能

  1. 经过ClassModifier处理后的byte[]数组才会传给HotSwapClassLoader.loadByte()方法进行类加载,byte[]数组在这里替换符号引用之后,与客户端直接在Java代码中引用HackSystem类再编译生成的Class是完全一样的。这样的实现既避免了客户端编写临时执行代码时要依赖特定的类(不然无法引入HackSystem),又避免了服务端修改标准输出后影响到其他程序的输出。
package com.testpak.RemoteExcutor;

/**
 * @author zZZ....
 * @description 只提供修改常量池常量的功能
 * @date 2023/3/21
 */
public class ClassModifier {
    /**
     * Class文件中常量池的起始偏移
     */
    private static final int CONSTANT_POOL_COUNT_INDEX = 8;
    /**
     * CONSTANT_Utf8_info常量的tag标志
     */
    private static final int CONSTANT_Utf8_info = 1;
    /**
     * 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为它不是定长的
     */
    private static final int[] CONSTANT_ITEM_LENGTH = { -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;
    }
    /**
     * 修改常量池中CONSTANT_Utf8_info常量的内容
     * @param oldStr 修改前的字符串
     * @param newStr 修改后的字符串
     * @return 修改结果
     */
    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_LENGTH[tag];
            }
        }
        return classByte;
    }
    /**
     * 获取常量池中常量的数量
     * @return 常量池数量
     */
    public int getConstantPoolCount() {
        return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
    }

}

添加数组处理工具

package com.testpak.RemoteExcutor;

/**
 * @author zZZ....
 * @description Bytes数组处理工具
 * @date 2023/3/21
 */
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 );
        return newBytes;
    }
}

改变输出流

package com.testpak.RemoteExcutor;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;

/**
 *为Javaclass劫持java.lang.System提供支持
 * 除了out和err外,其余的都直接转发给System处理
 */
public class HackSystem {
    public final static InputStream in = System.in;
    private 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);
    }
// 下面所有的方法都与java.lang.System的名称一样
// 实现都是字节转调System的对应方法
// 因版面原因,省略了其他方法
}

编写真正的执行器

  1. JavaclassExecuter,它是提供给外部调用的入口,调用前面几个支持类组装逻辑,完成类加载工作。JavaclassExecuter只有一个execute()方法,用输入的符合Class文件格式的byte[]数组替换掉java.lang.System的符号引用后,使用HotSwapClassLoader加载生成一个Class对象,由于每次执行execute()方法都会生成一个新的类加载器实例,因此同一个类可以实现重复加载
package com.testpak.RemoteExcutor;

import java.lang.reflect.Method;


/**
 * @description  Javaclass执行工具
 */
public class JavaclassExecuter {



    /**
     * 执行外部传过来的代表一个Java类的byte数组
     * 将输入类byte数组中代表 java.lang.System的CONTANT_Utf8_info常量修改为劫持后的HackSystem类
     * 反射调用需要执行的临时代码方法,输出结构为该类向System.out/err输出的信息
     * @param classByte
     * @return
     */
    public static String execute(byte[] classByte) {
        HackSystem.clearBuffer();
        ClassModifier classModifier = new ClassModifier(classByte);
        //修改Class字节码,把HackSystem 替代 System
        byte[] modiBytes = classModifier.modifyUTF8Constant("java.lang.System", "com.testpak.RemoteExcutor.HackSystem");
        HotSwapClassLoader loader = new HotSwapClassLoader();
        Class clazz = loader.loadByte(modiBytes);
        try {
            //调用其main方法
            Method method = clazz.getMethod("main", new Class[]{String[].class});
            method.invoke(null,String[].class);
        } catch (Exception e) {
            e.printStackTrace(HackSystem.out);
        }
        return HackSystem.getBufferString();

}}

服务器端的代码编写完毕,接下来就要检验一下。编写一个测试业务类,然后上传到服务器

package com.testpak.RemoteExcutor;


/**
 * 需要执行的临时代码类
 */
public class DemoTest {

    /**
     * 需要执行的业务方法
     * @param args null
     */
    public static void main(String[] args) {

        System.out.println("正在执行需要执行的业务方法");
        process1();
        process2();
    }

    public static void process1(){
        System.out.println("业务方法1...");
    }
    public static void process2(){
        System.out.println("业务方法2...");
    }

}

在tomcat的web项目的jsp目录下建一个jsp文件

然后在浏览器中访问即可,就可以在浏览器中看到这个类的运行结果了。
假设上业务类的存放目录为/data/test/DemoTest.class

<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.fenixsoft.classloading.execute.*" %>
<%
InputStream is = new FileInputStream("/data/test/DemoTest.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>");
%>

文章作者: Needle
转载声明:本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Needle !
  目录
  评论