Java_Sec Learn

xekOnerR Sleep.. zzzZZzZ

https://drun1baby.top
https://www.bilibili.com/video/BV16h411z7o9?spm_id_from=333.788.player.switch&vd_source=d51dbb41ef00391c5c021ee533eafd8e&p=2
https://github.com/Drun1baby/JavaSecurityLearning

Java 序列化

什么是序列化与反序列化

序列化: 对象 -> 字符串
反序列化: 字符串 -> 对象

为什么要进行序列化和反序列化

主要是用来传输数据

可以实现数据的持久化,通过序列化的方法可以把数据永久的保留。
可以利用序列化实现远程通信,即在网络上传送对象的字节序列。

基本反序列化实例

  • Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package Demo1;  // 修改成自己的 Package 路径  

import java.io.Serializable;

public class Person implements Serializable {

private transient String name;
private int age;

public Person(){
}

// 构造函数
public Person(String name, int age){
this.name = name;
this.age = age;
}

@Override
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
  • SerializationTest.java
    把序列化的代码封装进了 serialize 方法
    FileOutputStream 输出流对象, 在调用oos的writeObject方法进行序列化操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package Demo1;  


import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}
  • UnSerializationTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package Demo1;  

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializationTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}

注意:
序列化类需要实现Serializable接口
静态成员变量不能被序列化 (序列化是针对对象属性的,静态成员变量属于类)
transient标识的对象成员不参与序列化

序列化安全问题

可能存在漏洞的形式

  • 入口类 readObject直接调用危险方法
    序列化,反序列化后就会弹计算器
1
2
3
4
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {  
ois.defaultReadObject();
Runtime.getRuntime().exec("open -a Calculator"); // Macos
}
  • 入口参数中包含可控类,该类有危险方法,readObject时调用
  • 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
  • 构造函数/静态代码块等类加载时隐式执行

产生漏洞的攻击路线

攻击前提: 继承 Serializable

入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带)
找到入口类之后要找调用链 gadget chain 相同名称、相同类型
执行类 sink (RCE SSRF 写文件等等)比如 exec 这种函数

这里以 HashMap 为例子:
继承了 Serializable 接口
JAVA/attachments/Pasted image 20260127210044.png

找到 readObject 方法。
看到赋值给了 key 和 value,然后丢进了 hash 这个方法里。跟进
JAVA/attachments/Pasted image 20260127210638.png

满足 key 不为空,则 h = key.hashCode() ,继续跟进
JAVA/attachments/Pasted image 20260127234128.png

hashCode 位置处于 Object 类当中,满足我们 调用常见的函数 这一条件。
JAVA/attachments/Pasted image 20260127234211.png

分析 URLDNS 利用链

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
URLDNS 是 ysoserial 中一个利用链的名字,但是准确来说不能称为利用链;因为该利用链只作为探测是否存在反序列化漏洞使用。

1
2
3
4
5
6
7
8
9
10
11
/* URLDNS 利用链介绍
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*/

复现一下:

JAVA/attachments/Pasted image 20260128135805.png URL 是由 HashMap 的 `put` 方法产生的,所以我们先跟进 `put` 方法 JAVA/attachments/Pasted image 20260128135821.png 可以看到调用了 hash 方法,继续跟进 JAVA/attachments/Pasted image 20260128135853.png 看到调用了 key 的 `hashCode` 方法

在这里,key 实际上就是我们传入的 URL 地址,也就是 http://1sk79f.dnslog.cn
所以我们跟进 URL 查看 hashCode 方法

JAVA/attachments/Pasted image 20260128140043.png 看到调用的是 handler 的 `hashCode`,跟进查看 JAVA/attachments/Pasted image 20260128140236.png 继续跟进 JAVA/attachments/Pasted image 20260128140306.png 发现 `handler` 就是 `URLStreamHander` 抽象类中的。 我们再去找 `URLStreamHandler` 的 `hashCode` 方法。 JAVA/attachments/Pasted image 20260128140446.png

继续跟进 getHostAddress 后发现,就是根据 URL 获取主机名,ip 地址。
JAVA/attachments/Pasted image 20260128140526.png
JAVA/attachments/Pasted image 20260128143036.png

至此,整条 URLDNS 链就很清楚了:

1
2
3
4
5
HashMap -> readObject()
HashMap -> hash() (跟进put)
URL -> hashCode() (key.hashCode() 也就是 URL.hashCode())
URLStreamHandler -> hashCode() -> getHostAddress()
InetAddress -> getByName()

但是我们在序列化代码的时候,DNSLOG 居然接收到了
JAVA/attachments/Pasted image 20260128144217.png

为什么序列化(不进行序列化也会)的时候也会接收到 URLDNS 请求?

URL -> hashCode()

1
2
3
4
5
6
7
public synchronized int hashCode() {  
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

可以看到,如果这里 code 不是 -1,就不会发起请求。

这里先穿插讲一下反射, 可以先看完反射再看下面内容

要让这里不发起请求,需要让 url 对象 code 值不是 -1
序列化(也就是为了后面反序列化发起请求)的时候,需要让这个时候的 code 值为 -1

所以要通过反射的方式去操作,修改 url -> hashCode 的值不为 -1 ,然后进行 put

1
2
3
4
5
6
7
8
9
10
HashMap<URL, Integer> hashmap = new HashMap<>();  
URL url = new URL("http://y6jqkj.dnslog.cn");

Class c = url.getClass();
Field f = c.getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 123); // 从url修改 而不是原型c
hashmap.put(url,1);

serialize(hashmap);

验证:
JAVA/attachments/Pasted image 20260128164040.png

hashCode 在序列化的时候再变为 -1,再用上面那条命令修改值是否可行?

1
f.set(url, -1);

验证:
我们在 URL -> hashCode 处下断点,debug 反序列化测试类
JAVA/attachments/Pasted image 20260128165451.png

可以看到此时的 hashCode 确实是 -1

反序列化后触发了了 URLDNS
JAVA/attachments/Pasted image 20260128164338.png

注意:
如果出现:Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private int java.net.URL.hashCode accessible: module java.base does not "opens java.net" to unnamed module @12edcd21 报错
是因为 java 版本太高导致的

JAVA/attachments/Pasted image 20260128163423.png

添加

1
--add-opens java.base/java.net=ALL-UNNAMED

反射

反射的作用: 让 java 具有动态性
Java 的反射机制是指:对于任何一个类都能够知道它的所有属性和方法;并且对于任意一个类都能调用它的任意一个方法;这种动态获取信息以及调用的方法,称为 java 语言的反射。

Java 本身是一种静态语言

1
Student student = new Student();

反过来,python 就是动态语言

1
2
3
student = Student()  # 只要给个名字就行,不用声明类型
student = 123 # 甚至可以马上把它改成整数
student = "Hello" # 也可以改成字符串

正射与反射

  • 正射:
    我们在编写代码的时候,需要用到一个类的时候,都会先了解这个类是做什么的;然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。
1
2
Student student = new Student();
student.doHomework("Maths");
  • 反射:
    反射就是我们一开始不知道初始化的类对象是什么,自然也无法使用 new 关键词来创建对象;以创建 person 类(见上)反射作为例子讲解。
1
2
3
4
public static void main(String[] args) throws Exception {  
Person person = new Person();
Class c = person.getClass();
}

Class 是 c 的抽象, c 是 Class 的实例;
==所以反射就是操作 Class==

反射的使用方法

获取类的方法: forName
实例化类对象的方法:newInstance
获取函数的方法:getMethod
执行函数的方法:invoke

  • 实例化对象
1
2
3
4
5
6
public static void main(String[] args) throws Exception {  
Person person = new Person();
Class c = person.getClass();
Object o = c.newInstance();
System.out.println(o); // Person{name='null', age=0}
}
JAVA/attachments/Pasted image 20260128153135.png 但是 `newInstance` 是无参方法,无法传入参数。

我们一般使用 getConstructor 的方式来获取构造方法,然后实例化对象,即可传入参数。

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {  
Person person = new Person();
Class c = person.getClass();
Constructor personConstructor = c.getConstructor(String.class, int.class);
Person p = (Person) personConstructor.newInstance("test", 114514);
System.out.println(p); // Person{name='test', age=114514}
}

getConstructor 中需要填入 Person 中构造方法的参数
JAVA/attachments/Pasted image 20260128153807.png

  • 获取类里面属性JAVA/attachments/Pasted image 20260128154248.png

getFields 只打印 public :

1
2
3
4
5
Field[] personFields = c.getFields();  
for (Field f : personFields) {
System.out.println(f);
}
// public java.lang.String Demo1.Person.name

getDeclaredFields 打印所有变量:

1
2
3
4
5
6
7
Field[] personFields = c.getDeclaredFields();  
for (Field f : personFields) {
System.out.println(f);
}

// public java.lang.String Demo1.Person.name
// private int Demo1.Person.age

getField() / getDeclaredField() 相同,只不过变成了通过变量名获取

尝试修改 name:

1
2
3
4
Field nameFiled = c.getDeclaredField("name");  
nameFiled.set(p, "change");
System.out.println(p);
// Person{name='change', age=114514}

nameFiled.set 需要传入两个参数
JAVA/attachments/Pasted image 20260128155528.png

JAVA/attachments/Pasted image 20260128155226.png

但是如果我们要去修改 private int age; ,会报错,提示 age 属性为 private:
JAVA/attachments/Pasted image 20260128155712.png

但是反射给的权限还是很大的,只需要加上 setAccessible(true) 就可以修改,也就是暴力反射:

1
2
3
4
5
6
Field nameFiled = c.getDeclaredField("age");  
nameFiled.setAccessible(true);
nameFiled.set(p, 22);
System.out.println(p);

// Person{name='test', age=22}
  • 调用类里面的方法
    Person 类中定义了 action 方法:
1
2
3
public void action(String s){  
System.out.println(s);
}

获取所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Method[] methods = c.getMethods();  
for (Method m : methods) {
System.out.println(m);
}

/*
public java.lang.String Demo1.Person.toString()
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
public final void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
*/

也可以获取单个方法:
需要注意,和上面获取构造方法一样也需要传入一个泛型, 这里是告诉 getMethod 要传入的是 String 类型的参数
JAVA/attachments/Pasted image 20260128160857.png

1
2
3
4
Method method = c.getMethod("action",String.class);  
method.invoke(p,"outPut");

// outPut

关于私有方法也和上面修改私有属性值一样,需要修改为 Declared,并且设置访问权限

1
2
3
Method method = c.getDeclaredMethod("action",String.class);  
method.setAccessible(true);
method.invoke(p,"outPut");

反射小案例 - 反射弹 calc

正常 getRuntime , calc

1
Runtime.getRuntime().exec("calc");

反射:
先获取原型类 c ,然后通过 c 获取 exec 和 getRuntime 方法,然后先创建 getRuntime 的实例,然后再去 exec 调用命令。
需要注意的点只有获取 exec 方法的时候需要跟一个参数,直接跟进 Runtime 方法查看即可。

1
2
3
4
5
Class c = Class.forName("java.lang.Runtime");  
Method exec = c.getMethod("exec", String.class);
Method getruntime = c.getMethod("getRuntime");
Object runtime = getruntime.invoke(c);
exec.invoke(runtime, "C:\\WINDOWS\\System32\\calc.exe");

当然,我们也可以进行简化,写成(构式)代码,一行完成:

1
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")), "C:\\WINDOWS\\System32\\calc.exe");

Java - IO stream

IO 是指 Input/Output,即输入和输出。以内存为中心
为什么要把数据读到内存中才能处理这些数据?因为代码是在内存中运行的,数据也必须读到内存

先讲一下关于 Java 文件的一些操作。

创建文件的方式

  • new File(String pathName)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class IODemo1 {  
public static void main(String[] args) {
createFile();
}
public static void createFile(){
File file = new File("H:\\0x0B_FUXIAN\\Java\\JavaSec\\src\\IODemo\\temp.txt");
try {
file.createNewFile();
System.out.println("Create Successfully");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
  • new File(String path,File Name) , 大同小异
1
2
3
4
5
6
7
File abFile = new File("H:\\0x0B_FUXIAN\\Java\\JavaSec\\src\\IODemo");  
File file = new File(abFile,"tmp2.txt");
try {
file.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}

获取文件信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IODemo2 {  
public static void main(String[] args) {
getFileContents("H:\\0x0B_FUXIAN\\Java\\JavaSec\\src\\IODemo\\tmp2.txt");
}

public static void getFileContents(String filePath){
File file = new File(filePath);
System.out.println("File Name: " + file.getName());
System.out.println("File Absolute Path: " + file.getAbsolutePath());
System.out.println("File Parent Path " + file.getParent());
System.out.println("File Data: " + file.length());
System.out.println("Is a File: " + file.isFile());
System.out.println("Is a Directory: " + file.isDirectory());
}
}
JAVA/attachments/Pasted image 20260129113828.png

目录与文件操作

  • 文件删除 file.delete()
  • 目录删除 file.delete() 【只有空的目录可以删除成功】
  • 创建单级目录 file.mkdir()
  • 创建多级目录 file.mkdirs()

文件流的一些操作

  • 执行 whoami 并输出
1
2
3
4
5
6
7
8
InputStream inputStream = Runtime.getRuntime().exec("whoami").getInputStream();  
byte[] buffer = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
System.out.println(byteArrayOutputStream);
  • 读取文件内容并输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class IODemo4 {
public static void main(String[] args) throws IOException {
readFile();
}
public static void readFile() throws IOException {
String fileName = "H:\\0x0B_FUXIAN\\Java\\JavaSec\\src\\IODemo\\tmp2.txt";
int readData = 0;
FileInputStream fileInputStream = new FileInputStream(fileName);
while ((readData = fileInputStream.read()) != -1) {
System.out.print((char) readData);
}
fileInputStream.close();
}
}

这样操作英文(一个字节)字符没问题,但是中文(UTF-8)字符占三个字节,会出现乱码
要解决这一问题,需要理解原理
FileInputStream 读取的是字节流到内存中。
所以我们可以继续嵌套一层 InputStreamReader 来转换为字符流,然后在输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IODemo4 {  
public static void main(String[] args) throws IOException {
readFile();
}
public static void readFile() throws IOException {
String fileName = "H:\\0x0B_FUXIAN\\Java\\JavaSec\\src\\IODemo\\tmp2.txt";
int readData = 0;
FileInputStream fileInputStream = new FileInputStream(fileName);
InputStreamReader isr = new InputStreamReader(fileInputStream, "UTF-8");
while ((readData = isr.read()) != -1) {
System.out.print((char) readData);
}
fileInputStream.close();
}
}
  • 文件写入也很简单,和文件读取大差不差
1
2
3
4
5
6
7
8
9
10
11
Scanner scanner = new Scanner(System.in);  
File file = new File("H:\\0x0B_FUXIAN\\Java\\JavaSec\\src\\IODemo","tmp.txt");
file.createNewFile();

System.out.println("Enter String: ");
String s = scanner.nextLine();

OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
osw.write(s);
osw.close();
scanner.close();

Java 代理模式

静态代理

租房的人 -> 中介 ->(代理的方式) 房东 -> 房源

用 java 代码举例一下:

  • Rent.java (房源 - 注意是 interface)
1
2
3
public interface Rent {  
public void rent();
}
  • Host.java (房东)
1
2
3
4
5
6
public class Host implements Rent {  
@Override
public void rent() {
System.out.println("Rent the house.");
}
}
  • Proxy.java (中介
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Proxy {  
private Host host;

public Proxy(){
}

public Proxy(Host host){
this.host = host;
}

public void rent(){
host.rent();
}
}
  • Client.java
1
2
3
4
5
public static void main(String[] args) {  
Host host = new Host();
Proxy proxy = new Proxy(host);
proxy.rent();
}

Client 想租房直接找 Proxy 即可

还有一些功能是中介可以做的但房东不可以,比如说收中介费
但是需要添加很多的代码量
因此就有了动态代理:

JDK 动态代理

  • UserService.java 接口类
1
2
3
4
5
6
public interface UserService {  
public void add();
public void delete();
public void update();
public void query();
}
  • UserServiceImpl.java 接口实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserServiceImpl implements UserService {  
@Override
public void add() {
System.out.println("ADD");
}

@Override
public void delete() {
System.out.println("DELETE");
}

@Override
public void update() {
System.out.println("UPDATE");
}

@Override
public void query() {
System.out.println("QUERY");
}
}
  • UserInovacationHandler.java 动态代理实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserInvocationHandler implements InvocationHandler {  
UserServiceImpl userServiceImpl; // 代理的接口

public UserInvocationHandler() {
}

public UserInvocationHandler(UserServiceImpl userServiceImpl) {
this.userServiceImpl = userServiceImpl;
}

// 动态生成代理类实例 直接放在实现类里面了
public Object getProxy() {
Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), userServiceImpl.getClass().getInterfaces(), this);
return o;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
method.invoke(userServiceImpl,args);
return null;
}
}
  • ProxyTest.java
1
2
3
4
5
6
7
8
9
public class ProxyTest {  
public static void main(String[] args) {
UserServiceImpl userServiceImpl = new UserServiceImpl();
UserInvocationHandler userInvocationHandler = new UserInvocationHandler(userServiceImpl);
UserService proxy = (UserService) userInvocationHandler.getProxy(); // 动态生成代理类
proxy.add();
proxy.query();
}
}

动态代理在反序列化中的作用

态代理在反序列化当中的利用和 readObject 是异曲同工的

  • readObject 方法在反序列化当中会被自动执行
  • invoke 方法在动态代理当中会自动执行

如果说存在一个可以利用的类为 B.f ,比如 RunTime.exec 这种
入口类设置为 A ,最理想的情况下是 A[O] -> O.f,然后直接把 O 替换为 B 就可以执行了,但是在实战中这种情况是极少的。

但是如果入口类 A 存在 O.abc 这个方法,也就是 A[O] -> O.abcO 又是一个动态代理
那 O 的 invoke 方法里存在 .f 方法就可以漏洞利用了

也就是:

1
2
3
4
A[O] -> O.f = A[B] -> B.f  XXX

A[O] -> O.abc
O[O2] invoke -> O2.f = O[B] invoke -> B.f
  • Title: Java_Sec Learn
  • Author: xekOnerR
  • Created at : 2026-01-29 17:27:44
  • Updated at : 2026-01-29 18:23:02
  • Link: https://xekoner.xyz/2026/01/29/Java-sec/
  • License: This work is licensed under CC BY-NC-SA 4.0.