Noções básicas de serialização e desserialização Java

Vulnerabilidades de serialização e desserialização Java

Aqui, você aprenderá o que é serialização e desserialização Java, pensará da perspectiva do código e, em seguida, levará a problemas de segurança. Essa maneira de pensar contribui para melhorar sua compreensão das vulnerabilidades e também contribui para a auditoria de código. A direção está mais próxima . Claro, antes de estudar este artigo, você precisa ter uma certa base de programação Java, para que seja mais fácil de aprender.

visão geral

A serialização Java refere-se ao processo de conversão de objetos Java em sequências de bytes; a desserialização Java refere-se ao processo de restauração de sequências de bytes em objetos Java

A serialização é dividida em duas partes: serialização e [desserialização. A serialização é a primeira parte do processo, dividindo os dados em um fluxo de bytes para armazenamento em um arquivo ou transmissão através de uma rede. A desserialização consiste em abrir o fluxo de bytes e reconstruir o objeto. A serialização de objetos envolve não apenas a conversão de tipos de dados primitivos em representações de bytes, mas às vezes também a restauração dos dados. A restauração de dados requer uma instância de objeto dos dados restaurados.

Variáveis ​​de membro estático não podem ser serializadas porque a serialização é para objetos e variáveis ​​de membro estático pertencem a classes.

Variáveis ​​​​modificadas por transiente não podem ser serializadas

Implementação de caso de serialização e desserialização nativa

Vou criar 4 classes para obter a serialização do ponto de vista da programação. Essas 4 classes são Car, Student, PC1, PC2, onde PC1 simula o servidor 1, PC2 simula o servidor 2 e Student é um objeto. A simulação é passada em PC1 ObjectOutputStream serializa o objeto Student em um arquivo e, em seguida, desserializa o arquivo serializado de PC1 em um objeto Student por meio de ObjectInputStream em PC2.

No entanto, o pré-requisito para serialização e desserialização é que o objeto deve implementar a interface Serializable, caso contrário, um erro será relatado e o objeto serializado deve ter métodos get e set. Aqui estou usando o plug-in lombok, que possui nos métodos get e set

Serialização do ponto de vista do desenvolvedor

Car

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car implements Serializable {
    
    
    private String name;
    private int wheels;
}

Student

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Student implements Serializable {
    
    
  private String name;
  private int age;
  /**
  * Car 类也是需要实现序列化接口的。
  */
  private Car car;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
}

PC1

public class PC1 {
    
    
    public static void main(String[] args) throws IOException {
    
    
        Car car = new Car("BYD",4);
        Student yuanBoss = new Student("yuan_boss", 18,car);
        System.out.println(yuanBoss);
        serialize(yuanBoss);
    }
    public static void serialize(Student student) throws IOException {
    
    
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(student);
    }
}

Ao serializar o objeto Student, se o objeto Car no objeto Student não implementar Serializable, a seguinte exceção será relatada:

Depois de implementar Serializable, você pode ver a saída correta e que o arquivo ser.bin é gerado no diretório do projeto atual:

imagem-20230810171717269

imagem-20230810171841361

PC2

public class PC2 {
    
    
    public static void main(String[] args) throws IOException, ClassNotFoundException {
    
    
        unSerialize();
    }
    public static void unSerialize() throws IOException, ClassNotFoundException {
    
    
        ObjectInput oos = new ObjectInputStream(new FileInputStream("ser.bin"));
        Object obj = oos.readObject();
        System.out.println(obj);
    }
}

Depois de executar o código em PC2, podemos ver que o objeto Student foi gerado corretamente:

imagem-20230810171919144

Provocar pensamento

Por que o código acima é restaurado em um objeto após a desserialização? A razão é que o método readObject() é chamado, mas se reescrevermos o método readObject() na classe a ser serializada, ele será desserializado de acordo com a lógica que reescrevemos durante a desserialização.

Por exemplo, eu substituo os métodos writeObject() e readObject() na classe Student:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Student implements Serializable {
    
    
  private String name;
  private int age;
  /**
  * Car 类也是需要实现序列化接口的。
  */
  private Car car;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
  private void writeObject(ObjectOutputStream objectOutputStream){
    
    
    System.out.println("重写的writeObject方法");
  }
  private void readObject(ObjectInputStream objectInputStream){
    
    
    System.out.println("重写的readObject方法");
  }
  
}

Quando executamos PC1, o arquivo ser.bin não será gerado, mas 重写的writeObject方法esta frase será gerada. Após a execução de PC2, o método readObject() reescrito será chamado em vez de desserializado em um objeto.

Conforme mostrado na figura, execute PC1:

imagem-20230810174602996

Execute PC2:

imagem-20230810174627521

levanta preocupações de segurança

Através do pensamento acima, podemos saber que ao reescrever os métodos readObject() e writeObject(), a lógica dos métodos readObject() e writeObject() reescritos pode ser executada quando o programa é serializado ou desserializado, o que é equivalente ter a capacidade de executar código no servidor. É claro que para ter essa habilidade o objeto precisa atender às seguintes condições:

  1. Implementa a interface serializável
  2. Substitua o método readObject

Em Java, o HashMap atende a essa condição. Por que o HashMap reescreve o método readObject()? Porque quando o hashmap armazena objetos, ele precisa calcular o valor do hash para determinar o local de armazenamento do objeto. No entanto, os valores do hash calculados por máquinas diferentes são diferentes. O mesmo, portanto, para garantir que o mesmo objeto tenha o mesmo valor de hash em máquinas diferentes, o método readObject() é reescrito e as informações do objeto são retiradas durante a desserialização e então o valor do hash é calculado para garantir o valor hash do objeto Same.

imagem-20230810175807999

Portanto, podemos usar o HashMap para colocar alguns objetos maliciosos no HashMap. Quando o HashMap for desserializado, ele chamará automaticamente o método readObject() no HashMap para concluir o uso de alguns objetos maliciosos. Na cadeia de exploração CC6 e na cadeia URLDNS, o readObject() do HashMap é usado e, em seguida, a cadeia de exploração é acionada.

Caso de uso HashMap

Mestres que possuem o básico de serialização e desserialização Java podem tentar ler os seguintes artigos:

Análise de código-fonte de URLDNS escrito por chain EXP

Cadeia de utilização CC6 (a melhor cadeia de utilização CC) – ideias para escrever EXP – análise de código-fonte

Claro, ainda recomendo a leitura na ordem em que esta coluna está organizada, para obter os melhores resultados.

Acho que você gosta

Origin blog.csdn.net/weixin_46367450/article/details/132326437
Recomendado
Clasificación