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.
Diretório de artigos
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:
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:
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:
Execute PC2:
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:
- Implementa a interface serializável
- 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.
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
Claro, ainda recomendo a leitura na ordem em que esta coluna está organizada, para obter os melhores resultados.