Mecanismo de reflexão Java e questões de segurança
Prefácio 0x00
Recentemente, no processo de resumir a vulnerabilidade de desserialização do Java, o autor encontrou uma palavra "mecanismo de reflexão do Java" que não pode ser contornada.No passado, eu sabia apenas que esse era um mecanismo para implementar a linguagem "quase dinâmica" do Java, o que é conveniente para os desenvolvedores de depurar programas No entanto, eu não esperava que a "reflexão" tenha se tornado um dos métodos de ataques de vulnerabilidade à desserialização; portanto, neste artigo, resumirei os princípios do mecanismo de reflexão e quais outros problemas de segurança existem.
Mapa mental do mecanismo reflexo:
Definição do mecanismo de reflexão Java 0x01
Mecanismo de reflexão Java significa que, no estado de execução, para qualquer classe, você pode conhecer todas as propriedades e métodos dessa classe; para qualquer objeto, pode chamar qualquer um de seus métodos e propriedades; essas informações e dinâmicas obtidas dinamicamente A função de chamar o método do objeto é chamada de mecanismo de reflexão da linguagem java.
O ponto importante do mecanismo de reflexão é o "tempo de execução", que nos permite carregar, explorar e usar arquivos .class que são completamente desconhecidos durante a compilação enquanto o programa está em execução. Para resumir em uma frase, a reflexão pode realizar as propriedades e métodos de qualquer classe em tempo de execução.
0x02 vantagens e desvantagens do mecanismo de reflexão
Podemos ter uma pergunta quando somos novos na reflexão.Por que usar a reflexão para criar objetos diretamente? Ele não é perfumado? Desta vez, envolve os conceitos de compilação dinâmica e estática em Java. Vamos falar brevemente sobre isso.
- Compilação estática: determine o tipo em tempo de compilação e ligue o objeto.
- Compilação dinâmica: determine o tipo em tempo de execução e ligue o objeto. Maximiza a flexibilidade do Java, reflete o polimorfismo e reduz o acoplamento entre as classes.
Vantagens: O
mecanismo de reflexão pode realizar a criação dinâmica de objetos e compilação, mostrando muita flexibilidade, especialmente no desenvolvedor J2EE, sua flexibilidade é muito óbvia. Por exemplo, no desenvolvimento de um software em grande escala, quando o programa é compilado e lançado, se precisarmos atualizar algumas funções no futuro, não podemos pedir ao usuário que desinstale o software anterior e reinstale a nova versão. Se for estático, o programa inteiro precisará ser recompilado uma vez para obter a atualização da função e, se o mecanismo de reflexão for usado, ele poderá ser alcançado sem desinstalação dinâmica e só precisará ser criado e compilado dinamicamente em tempo de execução.
Desvantagens:
têm um impacto no desempenho. O mecanismo de reflexão é, na verdade, uma operação explicativa. Dizemos à JVM o que queremos fazer e como eles agrupam nossos requisitos. Esse tipo de operação é sempre mais lento do que apenas executar a mesma operação diretamente
0x03 Princípio do mecanismo de reflexão
A base principal do mecanismo de reflexão é entender a classe Class, que é um objeto de instância da classe java.lang.Class e Class é a classe de todas as classes. Para objetos comuns, geralmente usamos os seguintes métodos ao criar instâncias:
Demo test = new Demo();
Então, também podemos usar o método acima para criar um objeto de instância da classe class?
Class c = new Class();
A resposta não é boa, então analisamos o código fonte da Class e descobrimos que seu construtor é privado, o que significa que apenas a JVM pode criar objetos de Class.
Embora não possamos usar new para instanciar um objeto, como criar um objeto comum, podemos obter um objeto Class aleatoriamente uma classe existente.Existem três maneiras, a seguir: (supondo que exista uma classe comum chamada Demo)
- Instancie uma outra classe comum e chame getClass () nesta instância para obter o objeto Class
Demo test = new Demo();
Class c1 = test.getClass();
- Qualquer tipo de dado (incluindo tipos básicos de dados) possui um atributo de classe "estático", portanto, chame diretamente o atributo .class para obter o objeto Class
Class c2 = Demo.class;
- Chame o método forNmae da classe Class para obter o objeto Class
Class c3 = Class.forName(“ReflectDemo.Demo”)//forName()
O parâmetro é o caminho real, nome do pacote, nome da classe
Dos três métodos de criação, o terceiro geralmente é usado, porque o primeiro criou objetos e perdeu o significado do mecanismo de reflexão; o segundo precisa importar o pacote de classes, a dependência é muito forte e o pacote será lançado sem importação Erro de compilação. O terceiro método é geralmente usado.Uma string pode ser passada ou gravada no arquivo de configuração.
O princípio do mecanismo de reflexão é mapear vários componentes da classe java em objetos java individuais, para que possamos chamar todos os membros (variáveis, métodos) da classe em tempo de execução. A figura a seguir é o processo de carregamento de classes no mecanismo de reflexão:
Operação do mecanismo de reflexão Java 0x04
Através da introdução anterior, aprendemos como obter o objeto Class e, através do mecanismo de reflexão, você pode obter todas as informações dos membros do objeto Class e, em seguida, apresentamos brevemente algumas funções para obter membros:
- Obter método de membro
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
// Obtenha todos os métodos desta classe, excluindo a classe pai.
public Method getMethod(String name, Class<?>... parameterTypes)
// Obtenha todos os métodos públicos desta classe, incluindo a classe pai.
Os dois parâmetros são o nome do método e a lista de parâmetros de classe da classe de parâmetro de método (tipo de classe)
.Se houver quatro métodos de membro na classe A, a seguir:
use as funções getDeclaredMethod () e getMethod () para obter todos os métodos de membro / public na classe especificada
- Obter construtor
`public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)`
// Obtenha todos os construtores desta classe, excluindo o construtor de sua classe pai.
public Constructor<T> getConstructor(Class<?>... parameterTypes)
// Obtenha todos os construtores públicos desta classe, incluindo a classe pai.
Se houver três construtores na classe A, dois construtores públicos e um privado
usam a função getDeclaredConstructor () para obter todos os construtores; use a função getConstructor () para obter apenas construtores públicos.
- Obter variável de membro Campo
public Field getDeclaredField(String name)
// Pega todas as variáveis declaradas pela própria classe, excluindo as variáveis da sua classe pai.
public Field getField(String name)
// Obtenha todas as variáveis de membro público desta classe, incluindo suas variáveis de classe pai.
A variável membro da classe também é um objeto, que é um objeto de java.lang.reflect.Field, portanto, obtemos essas informações através do método encapsulado em java.lang.reflect.Field.
Existem algumas variáveis de membro
com atributos diferentes na classe A: use a função getDeclaredFields () para obter todas as variáveis de membro; use a função getFields () para obter apenas variáveis de membro públicas.
Mecanismo de reflexão 0x05 e vulnerabilidade à desserialização
A principal função da vulnerabilidade de desserialização:
writeObject () serialize, output Object to Byte stream
readObject () desserialize, output Byte stream to Object
Usando o mecanismo de reflexão, reescreva o método readObject, adicione a função Runtime.getRuntime () que pode executar a execução do comando, execute o comando calc.exe para acessar a calculadora
package reflectdemo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
public class demo implements Serializable{
private Integer age;
private String name;
public demo() {}
public demo(String name,Integer age){ //构造函数,初始化时执行
this.age = age;
this.name = name;
}
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject();//调用原始的readOject方法
try {//通过反射方法执行命令;
Method method= java.lang.Runtime.class.getMethod("exec", String.class);
Object result = method.invoke(Runtime.getRuntime(), "calc.exe");
}
catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args){
demo x= new demo();
operation.ser(x);
operation.deser();
}
}
class operation {
public static void ser(Object obj) {
try{//序列化操作,写数据
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
//ObjectOutputStream能把Object输出成Byte流
oos.writeObject(obj);//序列化关键函数
oos.flush(); //缓冲流
oos.close(); //关闭流
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void deser() {
try {//反序列化操作,读取数据
File file = new File("object.obj");
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
Object x = ois.readObject();//反序列化的关键函数
System.out.print(x);
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
A partir da vulnerabilidade de desserialização acima, pode-se observar que a reflexão Java pode realmente acessar métodos e propriedades particulares, o que é uma maneira de ignorar o mecanismo de segurança de segundo nível (um). Na verdade, é algo semelhante a um "backdoor" deixado pelo próprio Java para uma determinada finalidade ou para facilitar a depuração. De qualquer forma, seu princípio é desativar as verificações de segurança de acesso.
Em geral, quando encontramos uma vulnerabilidade e queremos que o programa implemente a execução de comandos, há duas direções nas quais podemos trabalhar duro
- Códigos e funções de controle: os dados são tratados como códigos, como furos de injeção, como injeção nomeada; ou readObject é reescrito como o código de demonstração acima, e o código personalizado é adicionado
- Controlar entrada, dados, variáveis: use as funções e a lógica existentes no código para controlar o processo, alterando a forma do conteúdo da entrada (entradas diferentes terão fluxos lógicos diferentes e executarão o código em diferentes blocos de código)
Para vulnerabilidades de desserialização de Java, isso pertence à categoria de entrada de dados de controle. Ao chamar o mecanismo de reflexão para acionar uma vulnerabilidade, ele tem dois pontos básicos que devem ser atendidos:
- Há uma classe serializável, e a classe reescreveu o método readObject () (como não há injeção de código, você pode descobrir se existe essa classe na lógica de código existente)
- A função method.invoke aparece na lógica do método readObject () reescrito e os parâmetros são controláveis.
Segurança de reflexão 0x06
Depois de ler o acima, devemos sinceramente suspirar que o mecanismo de reflexão Java é muito poderoso. No entanto, se tivermos um certo senso de segurança, descobriremos que o mecanismo Java é muito poderoso.
A segurança é uma questão mais complexa ao lidar com reflexões. A reflexão é freqüentemente usada pelo código do tipo de estrutura.Por isso, podemos desejar que a estrutura tenha acesso total ao código, independentemente das restrições de acesso convencionais. No entanto, em outros casos, o acesso não controlado pode representar sérios riscos à segurança.
Devido a esses requisitos conflitantes, a linguagem de programação Java define uma abordagem em vários níveis para lidar com a segurança da reflexão. O modo básico é implementar as mesmas restrições à reflexão aplicadas ao acesso ao código-fonte:
- Acesso de qualquer local a componentes comuns
- Nenhum acesso ao componente privado fora da própria classe
- Acesso limitado a componentes protegidos e empacotados (acesso padrão)
Comparado ao C ++, o Java é uma linguagem relativamente segura. Isso está intimamente relacionado ao seu mecanismo operacional: o C ++ é executado localmente, o que significa que as permissões de quase todos os programas são teoricamente iguais. Como o Java está sendo executado em uma máquina virtual e não entra em contato diretamente com o mundo externo, o ambiente de execução do Java é realmente um ambiente "sandbox". E, como modelo de segurança do Java, inclui: uma série de componentes de segurança, como verificador de bytecode, carregador de classes, gerenciador de segurança, controlador de acesso, etc., portanto, o mecanismo de segurança do Java parece ser mais complicado.