有关Java的小点
碎碎念:学Java真的好折磨人呜呜呜呜,好多好多好多包,名字也不认识,方法也不会用,只能慢慢一步步往前走,哭哭😭,这篇文章主要是总结一些关于Java的有趣小点,看个乐呵涨点冷知识就好
关于数字的类型转换
看到这个是因为2022中科大的CTF有一道猜数字的题目,关键代码如下
public class test {
public static void main(String[] args) throws XMLStreamException {
double rand = 随机数一个;
var guess=Double.parseDouble("用户输入的数据");
var isLess = rand < guess - 1e-6 / 2;
var isMore = rand > guess + 1e-6 / 2;
if(!isLess&&!isMore&&一次猜对){
giveFlag();
}
}
}
这里会随机生成一个0-1之间的double类型的随机数,用户需要猜其中的随机数是什么,并且只要一次猜对就会给flag,当然除非你运气已经好到一定地步了不然绝对不可能猜出来的,所以我们需要另辟蹊径了
可以看到用户输入的数据类型是以字符串输入然后交给parseDouble
函数转换成小数再进行判断的,那我们跟入parseDouble
中看一下解析流程,逐层调用到FloatingDecimal.readJavaFormatString
函数中,看到对特殊内容的解析部分
可以看到有三种特殊的解析类型,分别是NaN,Infinity和16进制,那我们分别用这几种试试看看最终效果
可以看到输入是NaN就能拿到flag,是因为NaN就像一个黑洞,任何数字和他进行运算结果都是NaN,Infinity也类似,不过在*0的时候会变成NaN,判断中Infinity大于任何数,而NaN表示非数值,所以不管进行什么运算结果都为NaN,布尔值为False
关于Java代码调用的问题
Java这个烂玩意让人困惑不是一天两天了,今天索性把Java中的代码调用问题一次性看完(调试完)
首先是static关键字定义的类,我们在使用的时候需要直接定义,如下:
className.staticInnerClass xxx=new className.staticInnerClass()//静态内部类只能访问外部静态属性或方法
如果是一个非静态子类,就需要先实例化一个父类,再实例化子类
className xxx = new className();
className.InnerClass inner = xxx.new InnerClass();
//或者一步到位
className.InnerClass innerObject = new className().new InnerClass();
synchronized
关于反射
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例:
// Main.java
import com.itranswarp.learnjava.Person;
public class Main {
String getFullName(Person p) {
return p.getFirstName() + " " + p.getLastName();
}
}
但是,如果不能获得Person
类,只有一个Object
实例,比如这样:
String getFullName(Object obj) {
return ???
}
但是如果我们尝试强制转型,将其转换为Person对象,我们就发现这时还是需要引入原始的类定义才能转换,而反射就是为了避免这种情况的出现
关于Java类型
对于Java来说除了int之类的基本类型之外,Java的其他类型全都是
class
类型那对于不同的数据类型之间,由于中间没有继承关系,所以互相无法进行赋值
而
class
是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class
类型时,将其加载进内存。每加载一种
class
,JVM就为其创建一个Class
类型的实例,并关联起来。注意:这里的Class
类型是一个名叫Class
的class
。它长这样:public final class Class { private Class() {} }
以
String
类为例,当JVM加载String
类时,它首先读取String.class
文件到内存,然后,为String
类创建一个Class
实例并关联起来:Class cls = new Class(String);
这个
Class
实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class
类的构造方法是private
,只有JVM能创建Class
实例,我们自己的Java程序是无法创建Class
实例的。所以,JVM持有的每个
Class
实例都指向一个数据类型(class
或interface
):┌───────────────────────────┐ │ Class Instance │──────> String ├───────────────────────────┤ │name = "java.lang.String" │ └───────────────────────────┘ ┌───────────────────────────┐ │ Class Instance │──────> Random ├───────────────────────────┤ │name = "java.util.Random" │ └───────────────────────────┘ ┌───────────────────────────┐ │ Class Instance │──────> Runnable ├───────────────────────────┤ │name = "java.lang.Runnable"│ └───────────────────────────┘
一个
Class
实例包含了该class
的所有完整信息:┌───────────────────────────┐ │ Class Instance │──────> String ├───────────────────────────┤ │name = "java.lang.String" │ ├───────────────────────────┤ │package = "java.lang" │ ├───────────────────────────┤ │super = "java.lang.Object" │ ├───────────────────────────┤ │interface = CharSequence...│ ├───────────────────────────┤ │field = value[],hash,... │ ├───────────────────────────┤ │method = indexOf()... │ └───────────────────────────┘
由于JVM为每个加载的
class
创建了对应的Class
实例,并在实例中保存了该class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class
实例,我们就可以通过这个Class
实例获取到该实例对应的class
的所有信息。这种通过
Class
实例获取class
信息的方法称为反射(Reflection)。
以上内容来自廖雪峰老师的Java教程,写的真的很不错,下面来写一些帮助我自己理解的内容
首先就是反射可以在java中执行的根本原因,就是Java会为在加载某个类的时候把类有关的信息存储起来,并且这部分就相当于是一个对象,我们可以通过反射访问到其中的内容,也就达成了获取其中的属性和方法的目的,又因为java数据都是通过字节码存储的,那么我们只需要按照字节码的生成规则对相应的位置的字节码进行修改就能达到修改内容的目的,并且还不会导致对应类的关联方法触发,这也是后面yso序列化链对反射大量使用的原因(防止链子生成的时候字节码就被执行)
如何获取一个
class
的Class
实例?有三个方法:方法一:直接通过一个
class
的静态变量class
获取:Class cls = String.class;
方法二:如果我们有一个实例变量,可以通过该实例变量提供的
getClass()
方法获取:String s = "Hello"; Class cls = s.getClass();
方法三:如果知道一个
class
的完整类名,可以通过静态方法Class.forName()
获取:Class cls = Class.forName("java.lang.String");
因为
Class
实例在JVM中是唯一的,所以,上述方法获取的Class
实例是同一个实例。可以用==
比较两个Class
实例:Class cls1 = String.class; String s = "Hello"; Class cls2 = s.getClass(); boolean sameClass = cls1 == cls2; // true
注意一下
Class
实例比较和instanceof
的差别:Integer n = new Integer(123); boolean b1 = n instanceof Integer; // true,因为n是Integer类型 boolean b2 = n instanceof Number; // true,因为n是Number类型的子类 boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
用
instanceof
不但匹配指定类型,还匹配指定类型的子类。而用==
判断class
实例可以精确地判断数据类型,但不能作子类型比较。通常情况下,我们应该用
instanceof
判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个class
的时候,我们才使用==
判断class
实例。因为反射的目的是为了获得某个实例的信息。因此,当我们拿到某个
Object
实例时,我们可以通过反射获取该Object
的class
信息:
关于代理
代理类是非常有意思的东西,之所以放在这里是因为代理的前提就是反射(废话,代理就在反射包下)
动态代理
我们在正常使用接口的时候都是先编写一个对应的实现类,然后再编写对应的方法,但是通过代理,我们就可以在不实现接口的前提下生成一个对象,并为其添加对应的方法
上面这个过程主要通过handler
实现
下面我们来看一下对于同样的功能我们传统的接口实现和使用代理有什么区别
定义接口:
public interface Hello { void morning(String name); }
编写实现类:
public class HelloWorld implements Hello { public void morning(String name) { System.out.println("Good morning, " + name); } }
创建实例,转型为接口并调用:
Hello hello = new HelloWorld(); hello.morning("Bob");
这种方式就是我们通常编写代码的方式。
还有动态代理
还有一种方式是动态代码,我们仍然先定义了接口
Hello
,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()
创建了一个Hello
接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。一个最简单的动态代理实现如下:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class Main { public static void main(String[] args) { InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method); if (method.getName().equals("morning")) { System.out.println("Good morning, " + args[0]); } return null; } }; Hello hello = (Hello) Proxy.newProxyInstance( Hello.class.getClassLoader(), // 传入ClassLoader new Class[] { Hello.class }, // 传入要实现的接口 handler); // 传入处理调用方法的InvocationHandler hello.morning("Bob"); } } interface Hello { void morning(String name); }
在运行期动态创建一个
interface
实例的方法如下:
定义一个
InvocationHandler
实例,它负责实现接口的方法调用;通过
Proxy.newProxyInstance()
创建
interface
实例,它需要3个参数:
- 使用的
ClassLoader
,通常就是接口类的ClassLoader
;- 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler
实例。将返回的
Object
强制转型为接口。
可以看到整个过程中我们根本没有对接口进行传统意义上的实现,而是动态的通过中间的handler处理器来实现类似方法的调用
关于RMI
Java的RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。RMI是Remote Method Invocation#远程方法调用的缩写。
提供服务的一方我们称之为服务器,而实现远程调用的一方我们称之为客户端。
我们先来实现一个最简单的RMI:服务器会提供一个WorldClock
服务,允许客户端获取指定时区的时间,即允许客户端调用下面的方法:
LocalDateTime getLocalDateTime(String zoneId);
要实现RMI,服务器和客户端必须共享同一个接口。我们定义一个WorldClock
接口,代码如下:
现在我们来尝试实现一个最简单的RMI,首先定义一个可使用RMI的接口如下
public interface WorldClock extends Remote {
LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
}
Java的RMI规定此接口必须派生自java.rmi.Remote
,并在每个方法声明抛出RemoteException
。
下面编写服务器的实现类,也就是我们客户端要调用的类
public interface WorldClock extends Remote {
LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
}
那么对RMI的内容我们就准备完毕,下面我们就要通过RMI提供的接口来吧上面的服务以RMI的形式部署到网络上,才能让客户端进行调用
public class Server {
public static void main(String[] args) throws RemoteException {
System.out.println("create World clock remote service...");
// 实例化一个WorldClock:
WorldClock worldClock = new WorldClockService();
// 将此服务转换为远程服务接口:
WorldClock skeleton = (WorldClock) UnicastRemoteObject.exportObject(worldClock, 0);
// 将RMI服务注册到1099端口:
Registry registry = LocateRegistry.createRegistry(1099);
// 注册此服务,服务名为"WorldClock":
registry.rebind("WorldClock", skeleton);
}
}
上述代码为服务端相关类,下面我们就要编写客户端代码
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
// 连接到服务器localhost,端口1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"WorldClock"的服务并强制转型为WorldClock接口:
WorldClock worldClock = (WorldClock/*共用接口*/) registry.lookup("WorldClock"/*服务名称*/);
// 正常调用接口方法:
LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai");
// 打印调用结果:
System.out.println(now);
}
}
运行就会发现我们的方法成功运行,我们打个断点来看看RMI是把对象加载到本地直接运行还是将数据交由远程处理
首先加载服务的时候服务端运行无报错,此时我们将服务端代码停止并且让客户端继续运行
发生报错运行失败,从上面的结果可见整个接口并没有将实例化后的对象或者实现好的类加载到本地执行,而是将其中对应的方法和属性由RMI中对应的接口交由服务端运行
从运行结果可知,因为客户端只有接口,并没有实现类,因此,客户端获得的接口方法返回值实际上是通过网络从服务器端获取的。整个过程实际上非常简单,对客户端来说,客户端持有的
WorldClock
接口实际上对应了一个“实现类”,它是由Registry
内部动态生成的,并负责把方法调用通过网络传递到服务器端。而服务器端接收网络调用的服务并不是我们自己编写的WorldClockService
,而是Registry
自动生成的代码。我们把客户端的“实现类”称为stub
,而服务器端的网络服务类称为skeleton
,它会真正调用服务器端的WorldClockService
,获取结果,然后把结果通过网络传递给客户端。整个过程由RMI底层负责实现序列化和反序列化:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌─────────────┐ ┌─────────────┐
│ │ Service │ │ │ │ Service │ │
└─────────────┘ └─────────────┘
│ ▲ │ │ ▲ │
│ │
│ │ │ │ │ │
┌─────────────┐ Network ┌───────────────┐ ┌─────────────┐
│ │ Client Stub ├─┼─────────┼>│Server Skeleton│──>│Service Impl │ │
└─────────────┘ └───────────────┘ └─────────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
Java的RMI严重依赖序列化和反序列化,而这种情况下可能会造成严重的安全漏洞,因为Java的序列化和反序列化不但涉及到数据,还涉及到二进制的字节码,即使使用白名单机制也很难保证100%排除恶意构造的字节码。因此,使用RMI时,双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务。
在使用RMI是需要注意的点
- 接口需要集成Remote接口,且方法需要抛出RemoteException错误
- 接口的实现类需要继承UnicastRemoteObject,同样的方法需要抛出RemoteException错误
- 如果远程方法需要传参,需要保证参数是可序列化的,我这里传参只是传了字符串,字符串是可序列化的,如果传参是自定义的对象,那么这个对象需要实现Serilizable接口
关于JNDI
关于JDBC
JDBC说白了就通过一套统一的API加上不同数据库的驱动,来实现用一套统一的API对不同种类的数据库进行连接
关于TemplatesImpl利用链RCE的问题
困惑很久了,主要是链子跟到最后一步不知道怎么最后RCE了
要想知道这个类为什么可以触发RCE,首先我们要知道默认情况下Java执行系统命令需要使用的是什么,代码应当如下
public class TouchFile{
public TouchFile() throws Exception {
Runtime.getRuntime().exec("calc");
}
}
直接使用Runtime包中的函数执行即可,将这个类编译成字节码后再进行base64,然后交给defineClass来加载字节码,再执行其中
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code =Base64.getDecoder().decode("yv66vgAAADQAHgoABgARCgASABMIABQKABIAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAYAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAOVG91Y2hGaWxlLmphdmEMAAcACAcAGQwAGgAbAQAEY2FsYwwAHAAdAQAJVG91Y2hGaWxlAQAQamF2YS9sYW5nL09iamVjdAEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAACAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAAEAAEABEADQASAAsAAAAEAAEADAAJAA0ADgACAAkAAAAmAAIAAQAAAAq4AAISA7YABFexAAAAAQAKAAAACgACAAAAFgAJABcACwAAAAQAAQAMAAEADwAAAAIAEA==");
Class yyds= (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "TouchFile", code, 0, code.length);
yyds.newInstance();
我们都知道 Java 的 ClassLoader 是用来加载字节码文件最基础的方法,ClassLoader 是什么呢?它就是一个“加载器”,告诉Java虚拟机如何加载这个类,用一句话概括它的作用就是将传入的字节码处理成真正的 Java 类然后返回。
ClassLoader
处理字节码的流程为 loadClass
-> findClass
-> defineClass
loadClass
::从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
findClass
: 根据基础URL指定的方式来加载类的字节码
defineClass
:处理前面传入的字节码,将其处理成真正的Java类
所以将字节码转为 java 类的其实是 defineClass 方法,翻看源码 ClassLoader#defineClass 是一个protected属性,无法直接在外部访问,只能通过反射的形式来调用,所以在实际场景中很难利用到它。
这也就是TemplatesImpl
存在的意义,我们直接去看这个类的定义
可以看到TransletClassLoader定义了defineClass方法,对其进行调用
再向前查找发现defineTransletClasses中生成了TransletClassLoader并调用了其中的defineClass方法,不过到这一步依然是private子类无法被外界直接调用,所以我们继续向上
最终找到newTransformer为public,可以直接被调用了,最终利用链如下
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
//未成功触发
TemplatesImpl#getTransletIndex() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
看完这个我们可以看看ysoserial最后是怎么通过他构造出来一个可供命令执行的字节码的
关于注解
什么是注解(Annotation)?注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”:
// this is a component:
@Resource("hello")
public class Hello {
@Inject
int n;
@PostConstruct
public void hello(@Param String name) {
System.out.println(name);
}
@Override
public String toString() {
return "Hello";
}
}
注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”。
从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
Java的注解可以分为三类:
第一类是由编译器使用的注解,例如:
@Override
:让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings
:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class
文件,它们在编译后就被编译器扔掉了。
第二类是由工具处理.class
文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class
文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct
的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
定义一个注解时,还可以定义配置参数。配置参数可以包括:
- 所有基本类型;
- String;
- 枚举类型;
- 基本类型、String、Class以及枚举的数组。
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外,大部分注解会有一个名为value
的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。
如果只写注解,相当于全部使用默认值。
举个栗子,对以下代码:
public class Hello {
@Check(min=0, max=100, value=55)
public int n;
@Check(value=99)
public int p;
@Check(99) // @Check(value=99)
public int x;
@Check
public int y;
}
@Check
就是一个注解。第一个@Check(min=0, max=100, value=55)
明确定义了三个参数,第二个@Check(value=99)
只定义了一个value
参数,它实际上和@Check(99)
是完全一样的。最后一个@Check
表示所有参数都使用默认值。
小结
注解(Annotation)是Java语言用于工具处理的标注:
注解可以配置参数,没有指定配置的参数使用默认值;
如果参数名称是value
,且只有一个参数,那么可以省略参数名称。