DNSPython : la boite à outil du DNS en python

Posted on ven. 19 avril 2019 in tuto

DNSPython est un module qui permet de forger et d'envoyer des requêtes DNS, des plus classiques aux plus complexes. Des fonctions de haut niveau vont permettre d'effectuer des requêtes courantes très simplement, et des fonctions de bas niveau permettant de créer sa requête pour prendre en compte les cas les plus complexes.

La documentation de DNSPython détaille les différents objets fournis par le module avec leurs attributs et leurs méthodes. Elle est complétée par quelques exemples. C'est suffisant, mais il est parfois un peu rude de se plonger dans la référence pour comprendre comment utiliser tous ces éléments. Voici donc quelques exemples que j'ai eu l'occasion d'implémenter.

Envoyer une requête simple

L'envoi de requête classique est simple : il suffit de choisir le type de requête et de l'envoyer au résolveur par défaut. Par exemple, pour une résolution IPv4, la commande dig -t A example.com +short donne :

import dns.name
import dns.resolver

n = dns.name.from_text('example.com')
answer = dns.resolver.query(n,'A')
for rdata in answer:
    print(rdata.to_text())

Et voilà ! Le script utilise notre serveur DNS par défaut pour effectuer la requête et afficher le ou les résultats. Si aucun résultat n'est trouvé, une exception dns.resolver.NoAnswer est levée.

Pour un autre type de requête, il suffit de changer le second paramètre de la méthode query. Par exemple, pour une requête MX, la commande dig -t MX gmail.com +short devient :

import dns.name
import dns.resolver

n = dns.name.from_text('gmail.com')
answer = dns.resolver.query(n,'MX')
for rdata in answer:
    print(rdata.to_text())

Voilà comment envoyer des requêtes de base avec DNSPython. Mais on peut faire bien plus intéressant !

Trouver les serveur faisant autorité sur un domaine

Trouver les serveurs faisant autorité sur un domaine est simple : il s'agit d'une requête NS qu'on peut réaliser sur le même format que l'exemple au-dessus.

Toutefois, si vous disposez d'un sous domaine contenant plusieurs particules (exemple de saison, portail.dgfip.finances.gouv.fr), on ne sait pas à quel niveau est le premier serveur de nom. Si on fait un dig -t NS portail.dgfip.finances.gouv.fr +short, on voit bien qu'on a aucun résultat. Il faut donc passer au domaine parent dgfip.finances.gouv.fr pour tester, puis si ce n'est toujours pas le cas finances.gouv.fr, etc... Si on atteint la racine ., c'est qu'il n'y a pas de serveur faisant autorité sur ce nom.

Cela nous donne le code python suivant :

import dns.name
import dns.resolver
n = dns.name.from_text('portail.dgfip.finances.gouv.fr')
try:
    while True:
        try:
            #On envoie une requête de type NS :
            answer = dns.resolver.query(n,'NS')
        except dns.resolver.NoAnswer:
            #On a pas trouvé d'enregistrement, on ignore l'exception et on continue
            print("Aucun enregistrement NS trouvé pour "+n.to_text()+", tentative avec le parent.")
        else:
            #Aucune exception levée, on a trouvé un enregistrement NS
            print("Enregistrement NS trouvé pour le domaine "+n.to_text())
            for rdata in answer:
                print(rdata.to_text())
            break;
        #Si on arrive ici, c'est qu'on a pas trouvé, on réessaie avec le parent:
        n = n.parent()
except dns.name.NoParent:
    #Cette exception est levée si le domaine n'a plus de parent, on a atteint la racine du DNS
    print("Aucun serveur NS trouvé")

Magie, on obtient directement le nom du ou des serveurs faisant autorité sur le nom :

Aucun enregistrement NS trouvé pour portail.dgfip.finances.gouv.fr., tentative avec le parent.
Enregistrement NS trouvé pour le domaine dgfip.finances.gouv.fr.
dns2.dgfip.finances.gouv.fr.
dns1.dgfip.finances.gouv.fr.

Vérifier si le serveur est protégé contre les amplifications DNS

L'amplification DNS est une attaque qui permet d'utiliser un serveur DNS vulnérable pour attaquer une cible tierce et provoquer un déni de service.

Le principe est simple : on envoie beaucoup de requêtes DNS courtes au serveur DNS, en lui faisant croire qu'elles viennent de notre cible (ce qui est plutôt facile puisque le protocole DNS utilise UDP par défaut). Le serveur va donc effectuer la résolution et envoyer le résultat à notre cible, qui va se retrouvée noyée sous les réponses. Cela permet de profiter de la bande passante du serveur DNS plutôt que d'envoyer les requêtes directement depuis la machine de l'attaquant.

En pratique, on utilise souvent la requête ".", qui permet d'obtenir les serveurs racine : la requête est très courte, mais la réponse est longue puisqu'elle liste les serveurs. Il suffit donc de tester si le serveur DNS qu'on veut utiliser répond bien à cette requête, comme avec la commande : dig . @192.168.1.1 +short

Par rapport aux requêtes précédentes, on ne va donc plus utiliser le résolveur par défaut de notre PC : on va créer un résolveur personnalisé auquel on va envoyer notre requête "." :

import dns.resolver

myResolver = dns.resolver.Resolver() #La création d'un résolveur personnalisé permet d'utiliser l'adresse de notre cible comme résolveur
myResolver.timeout = 5
myResolver.lifetime = 5
myResolver.nameservers = [ '192.168.1.1' ]
try:
    answer = myResolver.query('.')
except:
    print("Requête refusée par le serveur")
else:
    print("Le serveur a répondu :")
    for rdata in answer:
        print(rdata.to_text())

Heureusement (ou malheureusement, selon les points de vue), la plupart des serveurs DNS sont aujourd'hui paramétrés par défaut pour refuser les requêtes demandant les racines, donc dans la majorité des cas le résultat sera négatif.

Demander la version du serveur de noms

Les serveurs de noms sont des programmes polis et toujours ravis d'aider. C'est pour cela qu'il est possible grâce à une requête spécifique de demander leur version. En temps normal, on s'en fout un peu, mais d'un point de vue offensif, c'est intéressant puisque cela peut permettre de déceler une version vulnérable à une attaque.

Avec dig, la commande est dig @192.168.1.1 version.bind txt chaos +short, en remplaçant 192.168.1.1 par l'adresse de votre serveur de noms cible.

Une livebox étant bien sympa, cela nous permet d'obtenir la réponse :

dig @192.168.1.1 version.bind txt chaos +short
"dnsmasq-2.78"

On sait donc que la livebox Orange utilise dnsmasq en version 2.78 pour assurer la résolution dans le réseau local.

Il est possible (et conseillé) quand on gère un serveur de noms de modifier la réponse retournée par le serveur, mais ce n'est pas l'objet de cet article.

Comme on voit avec la commande dig, la requête est bien différente. En effet, on utilise la classe CHAOS qui permet de demander des infos sur le serveur. Le code résultant est donc un peu plus complexe : comme pour l'amplification, on n'utilise plus le résolveur par défaut de la machine mais notre cible, et en plus on veut une requête de classe CHAOS. Ça donne donc :

import dns.resolver

myResolver = dns.resolver.Resolver() #La création d'un résolveur personnalisé permet d'utiliser l'adresse de notre cible comme résolveur
myResolver.timeout = 5
myResolver.lifetime = 5
myResolver.nameservers = [ '192.168.1.1' ]
try:
    answer = myResolver.query('version.bind','TXT','CH') #Le troisième paramètre "CH" permet d'utiliser la classe CHAOS
except:
    print("Impossible de récupérer la version du serveur")
else:
    print("Réponse du serveur :")
    for rdata in answer:
        print(rdata.to_text())

Utile, on peut aussi récupérer le hostname du serveur en remplaçant version.bind par hostname.bind. Mais une fois encore, l'administrateur du serveur peut modifier ce que le serveur retourne pour renvoyer la signature d'un autre serveur, ou rien du tout.

Tenter un transfert de zone DNS

Le transfert de zone est une fonctionnalité du DNS qui permet de faciliter la réplication des entrées d'un serveur pour mettre en place une redondance. Mais, mal configurée, cela permet un attaquant de lister l'ensemble des entrées existantes pour un domaine.

Le transfert de zone se fait pour un domaine donné sur un de ses serveurs d'autorité. Avec dig, la commande est dig -t axfr zonetransfer.me @nsztm2.digi.ninja. (le domaine zonetransfer.me est volontairement vulnérable à cette attaque, ce qui permet de tester facilement).

Avec DNSPython, c'est très simple :

import dns.zone
import dns.query

try:
    z = dns.zone.from_xfr(dns.query.xfr('nsztm2.digi.ninja','zonetransfer.me'))
except:
    print("Echec du tranfert")
else:
    print("Succès du transfert")
    names = z.nodes.keys()
    for n in names:
        print(z[n].to_text(n))

Et voilà, on récupère toutes les informations sur la zone !

DNSPython est très puissant et permet de faire facilement des requêtes simples ou complexes. C'est le genre d'outil hyper pratique pour les pentesters et les administrateurs systèmes, et ces quelques exemples montrent bien ses nombreuses possibilités.