├── .gitignore ├── CHANGELOG ├── LICENSE ├── README.md ├── dnschef.ini └── dnschef.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | .DS_Store 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | __pycache__ 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 0.3 2 | 3 | * Added support for the latest version of the dnslib library - 0.9.3 4 | * Added support for logging. (idea by kafeine) 5 | * Added support for SRV, DNSKEY, and RRSIG records. (idea by mubix) 6 | * Added support for TCP remote nameserver connections. (idea by mubix) 7 | * DNS name matching is now case insensitive. 8 | * Various small bug fixes and performance tweaks. 9 | * Python libraries are no longer bundled with the distribution, but 10 | compiled in the Windows binary. 11 | 12 | Version 0.2.1 13 | 14 | * Fixed a Python 2.6 compatibility issue. (thanks Mehran Goudarzi) 15 | 16 | Version 0.2 17 | 18 | * Added IPv6 support. 19 | * Added AAAA, MX, CNAME, NS, SOA and NAPTR support. 20 | * Added support for ANY queries (returns all known fake records). 21 | * Changed file format to support more DNS record types. 22 | * Added alternative DNS port support (contributed by fnv). 23 | * Added alternative listening port support for the server (contributed by Mark Straver). 24 | * Updated bundled dnslib library to the latest version - 0.8.2. 25 | * Included IPy library for IPv6 support. 26 | 27 | Version 0.1 28 | 29 | * First public release 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Peter Kacherginsky 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 3. Neither the name of the copyright holder nor the names of its contributors 13 | may be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a fork of the DNSChef project v0.2.1 hosted at: http://thesprawl.org/projects/dnschef/ 2 | 3 | ## Overview 4 | 5 | DNSChef is a highly configurable DNS proxy for Penetration Testers and Malware Analysts. A DNS proxy (aka "Fake DNS") is a tool used for application network traffic analysis among other uses. For example, a DNS proxy can be used to fake requests for "badguy.com" to point to a local machine for termination or interception instead of a real host somewhere on the Internet. 6 | 7 | There are several DNS Proxies out there. Most will simply point all DNS queries a single IP address or implement only rudimentary filtering. DNSChef was developed as part of a penetration test where there was a need for a more configurable system. As a result, DNSChef is cross-platform application capable of forging responses based on inclusive and exclusive domain lists, supporting multiple DNS record types, matching domains with wildcards, proxying true responses for nonmatching domains, defining external configuration files, IPv6 and many other features. You can find detailed explanation of each of the features and suggested uses below. 8 | 9 | The use of DNS Proxy is recommended in situations where it is not possible to force an application to use some other proxy server directly. For example, some mobile applications completely ignore OS HTTP Proxy settings. In these cases, the use of a DNS proxy server such as DNSChef will allow you to trick that application into forwarding connections to the desired destination. 10 | 11 | Version 0.3 introduces support for more DNS record types, DNSSEC, logging, more configurable remote nameservers, support for the updated dnslib library and several bug fixes. 12 | 13 | Version 0.2 introduces IPv6 support, large number of new DNS record types, custom ports and other frequently requested features. 14 | 15 | ## Table of Contents 16 | 17 | - Setting up a DNS Proxy 18 | - Installing DNSChef 19 | - Running DNSChef 20 | - Intercept all responses 21 | - Filtering domains 22 | - Reverse filtering 23 | - External definitions file 24 | - Advanced Filtering 25 | - Logging 26 | - Other configurations 27 | - Internal architecture 28 | 29 | ## Setting up a DNS Proxy 30 | 31 | Before you can start using DNSChef, you must configure your machine to use a DNS nameserver with the tool running on it. You have several options based on the operating system you are going to use: 32 | 33 | - Linux - Edit /etc/resolv.conf to include a line on the very top with your traffic analysis host (e.g add "nameserver 127.0.0.1" if you are running locally). Alternatively, you can add a DNS server address using tools such as Network Manager. Inside the Network Manager open IPv4 Settings, select Automatic (DHCP) addresses only or Manual from the Method drop down box and edit DNS Servers text box to include an IP address with DNSChef running. 34 | - Windows - Select Network Connections from the Control Panel. Next select one of the connections (e.g. "Local Area Connection"), right-click on it and select properties. From within a newly appearing dialog box, select Internet Protocol (TCP/IP) and click on properties. At last select Use the following DNS server addresses radio button and enter the IP address with DNSChef running. For example, if running locally enter 127.0.0.1. 35 | - OS X - Open System Preferences and click on the Network icon. Select the active interface and fill in the DNS Server field. If you are using Airport then you will have to click on Advanced... button and edit DNS servers from there. Alternatively, you can edit /etc/resolv.conf and add a fake nameserver to the very top there (e.g "nameserver 127.0.0.1"). 36 | - iOS - Open Settings and select General. Next select on Wi-Fi and click on a blue arrow to the right of an active Access Point from the list. Edit DNS entry to point to the host with DNSChef running. Make sure you have disabled Cellular interface (if available). 37 | - Android - Open Settings and select Wireless and network. Click on Wi-Fi settings and select Advanced after pressing the Options button on the phone. Enable Use static IP checkbox and configure a custom DNS server. 38 | 39 | If you do not have the ability to modify device's DNS settings manually, then you still have several options involving techniques such as ARP Spoofing, Rogue DHCP and other creative methods. 40 | 41 | At last you need to configure a fake service where DNSChef will point all of the requests. For example, if you are trying to intercept web traffic, you must bring up either a separate web server running on port 80 or set up a web proxy (e.g. Burp) to intercept traffic. DNSChef will point queries to your proxy/server host with properly configured services. 42 | 43 | ## Installing DNSChef 44 | 45 | DNSChef requires dnslib and IPy python libraries. You can obtain their latest versions here: 46 | 47 | - dnslib: https://bitbucket.org/paulc/dnslib 48 | - IPy: https://github.com/haypo/python-ipy/wiki 49 | 50 | 51 | ## Running DNSChef 52 | 53 | DNSChef is a cross-platform application developed in Python which should run on most platforms which have a Python interpreter. You can use the supplied dnschef.exe executable to run it on Windows hosts without installing a Python interpreter. This guide will concentrate on Unix environments; however, all of the examples below were tested to work on Windows as well. 54 | 55 | Let's get a taste of DNSChef with its most basic monitoring functionality. Execute the following command as root (required to start a server on port 53): 56 | ``` 57 | # ./dnschef.py 58 | 59 | _ _ __ 60 | | | version 0.2 | | / _| 61 | __| |_ __ ___ ___| |__ ___| |_ 62 | / _` | '_ \/ __|/ __| '_ \ / _ \ _| 63 | | (_| | | | \__ \ (__| | | | __/ | 64 | \__,_|_| |_|___/\___|_| |_|\___|_| 65 | iphelix@thesprawl.org 66 | 67 | [*] DNSChef started on interface: 127.0.0.1 68 | [*] Using the following nameservers: 8.8.8.8 69 | [*] No parameters were specified. Running in full proxy mode 70 | ``` 71 | 72 | Without any parameters, DNSChef will run in full proxy mode. This means that all requests will simply be forwarded to an upstream DNS server (8.8.8.8 by default) and returned back to the quering host. For example, let's query an "A" record for a domain and observe results: 73 | ``` 74 | $ host -t A thesprawl.org 75 | thesprawl.org has address 108.59.3.64 76 | ``` 77 | 78 | DNSChef will print the following log line showing time, source IP address, type of record requested and most importantly which name was queried: 79 | ``` 80 | [23:54:03] 127.0.0.1: proxying the response of type 'A' for thesprawl.org 81 | ``` 82 | 83 | This mode is useful for simple application monitoring where you need to figure out which domains it uses for its communications. 84 | 85 | DNSChef has full support for IPv6 which can be activated using -6 or --ipv6* flags. It works exactly as IPv4 mode with the exception that default listening interface is switched to ::1 and default DNS server is switched to 2001:4860:4860::8888. Here is a sample output: 86 | ``` 87 | # ./dnschef.py -6 88 | _ _ __ 89 | | | version 0.2 | | / _| 90 | __| |_ __ ___ ___| |__ ___| |_ 91 | / _` | '_ \/ __|/ __| '_ \ / _ \ _| 92 | | (_| | | | \__ \ (__| | | | __/ | 93 | \__,_|_| |_|___/\___|_| |_|\___|_| 94 | iphelix@thesprawl.org 95 | 96 | [*] Using IPv6 mode. 97 | [*] DNSChef started on interface: ::1 98 | [*] Using the following nameservers: 2001:4860:4860::8888 99 | [*] No parameters were specified. Running in full proxy mode 100 | [00:35:44] ::1: proxying the response of type 'A' for thesprawl.org 101 | [00:35:44] ::1: proxying the response of type 'AAAA' for thesprawl.org 102 | [00:35:44] ::1: proxying the response of type 'MX' for thesprawl.org 103 | ``` 104 | 105 | NOTE: By default, DNSChef creates a UDP listener. You can use TCP instead with the --tcp argument discussed later. 106 | 107 | 108 | ### Intercept All Responses 109 | 110 | Now, that you know how to start DNSChef let's configure it to fake all replies to point to 127.0.0.1 using the --fakeip parameter: 111 | ``` 112 | # ./dnschef.py --fakeip 127.0.0.1 -q 113 | [*] DNSChef started on interface: 127.0.0.1 114 | [*] Using the following nameservers: 8.8.8.8 115 | [*] Cooking all A replies to point to 127.0.0.1 116 | [23:55:57] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1 117 | [23:55:57] 127.0.0.1: proxying the response of type 'AAAA' for google.com 118 | [23:55:57] 127.0.0.1: proxying the response of type 'MX' for google.com 119 | ``` 120 | 121 | In the above output you an see that DNSChef was configured to proxy all requests to 127.0.0.1. The first line of log at 08:11:23 shows that we have "cooked" the "A" record response to point to 127.0.0.1. However, further requests for 'AAAA' and 'MX' records are simply proxied from a real DNS server. Let's see the output from requesting program: 122 | ``` 123 | $ host google.com localhost 124 | google.com has address 127.0.0.1 125 | google.com has IPv6 address 2001:4860:4001:803::1001 126 | google.com mail is handled by 10 aspmx.l.google.com. 127 | google.com mail is handled by 40 alt3.aspmx.l.google.com. 128 | google.com mail is handled by 30 alt2.aspmx.l.google.com. 129 | google.com mail is handled by 20 alt1.aspmx.l.google.com. 130 | google.com mail is handled by 50 alt4.aspmx.l.google.com. 131 | ``` 132 | 133 | As you can see the program was tricked to use 127.0.0.1 for the IPv4 address. However, the information obtained from IPv6 (AAAA) and mail (MX) records appears completely legitimate. The goal of DNSChef is to have the least impact on the correct operation of the program, so if an application relies on a specific mailserver it will correctly obtain one through this proxied request. 134 | 135 | Let's fake one more request to illustrate how to target multiple records at the same time: 136 | ``` 137 | # ./dnschef.py --fakeip 127.0.0.1 --fakeipv6 ::1 -q 138 | [*] DNSChef started on interface: 127.0.0.1 139 | [*] Using the following nameservers: 8.8.8.8 140 | [*] Cooking all A replies to point to 127.0.0.1 141 | [*] Cooking all AAAA replies to point to ::1 142 | [00:02:14] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1 143 | [00:02:14] 127.0.0.1: cooking the response of type 'AAAA' for google.com to ::1 144 | [00:02:14] 127.0.0.1: proxying the response of type 'MX' for google.com 145 | ``` 146 | 147 | In addition to the --fakeip flag, I have now specified --fakeipv6 designed to fake 'AAAA' record queries. Here is an updated program output: 148 | ``` 149 | $ host google.com localhost 150 | google.com has address 127.0.0.1 151 | google.com has IPv6 address ::1 152 | google.com mail is handled by 10 aspmx.l.google.com. 153 | google.com mail is handled by 40 alt3.aspmx.l.google.com. 154 | google.com mail is handled by 30 alt2.aspmx.l.google.com. 155 | google.com mail is handled by 20 alt1.aspmx.l.google.com. 156 | google.com mail is handled by 50 alt4.aspmx.l.google.com. 157 | ``` 158 | 159 | Once more all of the records not explicitly overriden by the application were proxied and returned from the real DNS server. However, IPv4 (A) and IPv6 (AAAA) were both faked to point to a local machine. 160 | 161 | DNSChef supports multiple record types from the command line: 162 | ``` 163 | +--------+--------------+-----------+--------------------------+ 164 | | Record | Description |Argument | Example | 165 | +--------+--------------+-----------+--------------------------+ 166 | | A | IPv4 address |--fakeip | --fakeip 192.0.2.1 | 167 | | AAAA | IPv6 address |--fakeipv6 | --fakeipv6 2001:db8::1 | 168 | | MX | Mail server |--fakemail | --fakemail mail.fake.com | 169 | | CNAME | CNAME record |--fakealias| --fakealias www.fake.com | 170 | | NS | Name server |--fakens | --fakens ns.fake.com | 171 | +--------+--------------+-----------+--------------------------+ 172 | ``` 173 | 174 | NOTE: For usability not all DNS record types are exposed on the command line. Additional records such as PTR, TXT, SOA, SRV, DNSKEY, RRSIG, etc. can be specified using the --file flag and an appropriate record header. See the external definitions file section below for details. 175 | 176 | At last let's observe how the application handles queries of type ANY: 177 | ``` 178 | # ./dnschef.py --fakeip 127.0.0.1 --fakeipv6 ::1 --fakemail mail.fake.com --fakealias www.fake.com --fakens ns.fake.com -q 179 | [*] DNSChef started on interface: 127.0.0.1 180 | [*] Using the following nameservers: 8.8.8.8 181 | [*] Cooking all A replies to point to 127.0.0.1 182 | [*] Cooking all AAAA replies to point to ::1 183 | [*] Cooking all MX replies to point to mail.fake.com 184 | [*] Cooking all CNAME replies to point to www.fake.com 185 | [*] Cooking all NS replies to point to ns.fake.com 186 | [00:17:29] 127.0.0.1: cooking the response of type 'ANY' for google.com with all known fake records. 187 | ``` 188 | 189 | DNS ANY record queries results in DNSChef returning every faked record that it knows about for an applicable domain. Here is the output that the program will see: 190 | ``` 191 | $ host -t ANY google.com localhost 192 | google.com has address 127.0.0.1 193 | google.com has IPv6 address ::1 194 | google.com mail is handled by 10 mail.fake.com. 195 | google.com is an alias for www.fake.com. 196 | google.com name server ns.fake.com. 197 | ``` 198 | 199 | ### Filtering Domains 200 | 201 | Using the above example, consider you only want to intercept requests for thesprawl.org and leave queries to all other domains such as webfaction.com without modification. You can use the --fakedomains parameter as illustrated below: 202 | ``` 203 | # ./dnschef.py --fakeip 127.0.0.1 --fakedomains thesprawl.org -q 204 | [*] DNSChef started on interface: 127.0.0.1 205 | [*] Using the following nameservers: 8.8.8.8 206 | [*] Cooking replies to point to 127.0.0.1 matching: thesprawl.org 207 | [00:23:37] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 127.0.0.1 208 | [00:23:52] 127.0.0.1: proxying the response of type 'A' for mx9.webfaction.com 209 | From the above example the request for thesprawl.org was faked; however, the request for mx9.webfaction.com was left alone. Filtering domains is very useful when you attempt to isolate a single application without breaking the rest. 210 | ``` 211 | 212 | NOTE: DNSChef will not verify whether the domain exists or not before faking the response. If you have specified a domain it will always resolve to a fake value whether it really exists or not. 213 | 214 | ### Reverse Filtering 215 | 216 | In another situation you may need to fake responses for all requests except a defined list of domains. You can accomplish this task using the --truedomains parameter as follows: 217 | ``` 218 | # ./dnschef.py --fakeip 127.0.0.1 --truedomains thesprawl.org,*.webfaction.com -q 219 | [*] DNSChef started on interface: 127.0.0.1 220 | [*] Using the following nameservers: 8.8.8.8 221 | [*] Cooking replies to point to 127.0.0.1 not matching: *.webfaction.com, thesprawl.org 222 | [00:27:57] 127.0.0.1: proxying the response of type 'A' for mx9.webfaction.com 223 | [00:28:05] 127.0.0.1: cooking the response of type 'A' for google.com to 127.0.0.1 224 | There are several things going on in the above example. First notice the use of a wildcard (). All domains matching .webfaction.com will be reverse matched and resolved to their true values. The request for 'google.com' returned 127.0.0.1 because it was not on the list of excluded domains. 225 | ``` 226 | 227 | NOTE: Wildcards are position specific. A mask of type .thesprawl.org will match www.thesprawl.org but not www.test.thesprawl.org. However, a mask of type .*.thesprawl.org will match thesprawl.org, www.thesprawl.org and www.test.thesprawl.org. 228 | 229 | ### External Definitions File 230 | 231 | There may be situations where defining a single fake DNS record for all matching domains may not be sufficient. You can use an external file with a collection of DOMAIN=RECORD pairs defining exactly where you want the request to go. 232 | 233 | For example, let create the following definitions file and call it dnschef.ini: 234 | ``` 235 | [A] 236 | *.google.com=192.0.2.1 237 | thesprawl.org=192.0.2.2 238 | *.wordpress.*=192.0.2.3 239 | ``` 240 | 241 | Notice the section header [A], it defines the record type to DNSChef. Now let's carefully observe the output of multiple queries: 242 | ``` 243 | # ./dnschef.py --file dnschef.ini -q 244 | [*] DNSChef started on interface: 127.0.0.1 245 | [*] Using the following nameservers: 8.8.8.8 246 | [+] Cooking A replies for domain *.google.com with '192.0.2.1' 247 | [+] Cooking A replies for domain thesprawl.org with '192.0.2.2' 248 | [+] Cooking A replies for domain *.wordpress.* with '192.0.2.3' 249 | [00:43:54] 127.0.0.1: cooking the response of type 'A' for google.com to 192.0.2.1 250 | [00:44:05] 127.0.0.1: cooking the response of type 'A' for www.google.com to 192.0.2.1 251 | [00:44:19] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 192.0.2.2 252 | [00:44:29] 127.0.0.1: proxying the response of type 'A' for www.thesprawl.org 253 | [00:44:40] 127.0.0.1: cooking the response of type 'A' for www.wordpress.org to 192.0.2.3 254 | [00:44:51] 127.0.0.1: cooking the response of type 'A' for wordpress.com to 192.0.2.3 255 | [00:45:02] 127.0.0.1: proxying the response of type 'A' for slashdot.org 256 | ``` 257 | 258 | Both google.com and www.google.com matched the *.google.com entry and correctly resolved to 192.0.2.1. On the other hand www.thesprawl.org request was simply proxied instead of being modified. At last all variations of wordpress.com, www.wordpress.org, etc. matched the *.wordpress.* mask and correctly resolved to 192.0.2.3. At last an undefined slashdot.org query was simply proxied with a real response. 259 | 260 | You can specify section headers for all other supported DNS record types including the ones not explicitly exposed on the command line: [A], [AAAA], [MX], [NS], [CNAME], [PTR], [NAPTR] and [SOA]. For example, let's define a new [PTR] section in the 'dnschef.ini' file: 261 | ``` 262 | [PTR] 263 | *.2.0.192.in-addr.arpa=fake.com 264 | ``` 265 | 266 | Let's observe DNSChef's behavior with this new record type: 267 | ``` 268 | ./dnschef.py --file dnschef.ini -q 269 | [sudo] password for iphelix: 270 | [*] DNSChef started on interface: 127.0.0.1 271 | [*] Using the following nameservers: 8.8.8.8 272 | [+] Cooking PTR replies for domain *.2.0.192.in-addr.arpa with 'fake.com' 273 | [00:11:34] 127.0.0.1: cooking the response of type 'PTR' for 1.2.0.192.in-addr.arpa to fake.com 274 | ``` 275 | 276 | And here is what a client might see when performing reverse DNS queries: 277 | ``` 278 | $ host 192.0.2.1 localhost 279 | 1.2.0.192.in-addr.arpa domain name pointer fake.com. 280 | ``` 281 | Some records require exact formatting. Good examples are SOA, NAPTR, and SRV records: 282 | ``` 283 | [SOA] 284 | *.thesprawl.org=ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600 285 | 286 | [NAPTR] 287 | *.thesprawl.org=100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! . 288 | 289 | [SRV] 290 | ; FORMAT: priority weight port target 291 | *.*.thesprawl.org=0 5 5060 sipserver.fake.com 292 | ``` 293 | 294 | ### Advanced Filtering 295 | 296 | You can mix and match input from a file and command line. For example the following command uses both --file and --fakedomains parameters: 297 | ``` 298 | # ./dnschef.py --file dnschef.ini --fakeip 6.6.6.6 --fakedomains=thesprawl.org,slashdot.org -q 299 | [*] DNSChef started on interface: 127.0.0.1 300 | [*] Using the following nameservers: 8.8.8.8 301 | [+] Cooking A replies for domain *.google.com with '192.0.2.1' 302 | [+] Cooking A replies for domain thesprawl.org with '192.0.2.2' 303 | [+] Cooking A replies for domain *.wordpress.* with '192.0.2.3' 304 | [*] Cooking A replies to point to 6.6.6.6 matching: *.wordpress.*, *.google.com, thesprawl.org 305 | [*] Cooking A replies to point to 6.6.6.6 matching: slashdot.org, *.wordpress.*, *.google.com, thesprawl.org 306 | [00:49:05] 127.0.0.1: cooking the response of type 'A' for google.com to 192.0.2.1 307 | [00:49:15] 127.0.0.1: cooking the response of type 'A' for slashdot.org to 6.6.6.6 308 | [00:49:31] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 6.6.6.6 309 | [00:50:08] 127.0.0.1: proxying the response of type 'A' for tor.com 310 | ``` 311 | 312 | Notice the definition for thesprawl.org in the command line parameter took precedence over dnschef.ini. This could be useful if you want to override values in the configuration file. slashdot.org still resolves to the fake IP address because it was specified in the --fakedomains parameter. tor.com request is simply proxied since it was not specified in either command line or the configuration file. 313 | See sample dnschef.ini file for additional examples. 314 | 315 | ### Logging 316 | 317 | DNSChef is capable of storing activity log in an external file using the --logfile log1.txt command line parameter. Below is a snippet of a sample DNSChef session: 318 | ``` 319 | [05/Nov/2014:22:00:49 -0800] DNSChef is active. 320 | [05/Nov/2014:22:01:07 -0800] 127.0.0.1: cooking the response of type 'A' for thesprawl.org to 192.0.2.1 321 | [05/Nov/2014:22:07:24 -0800] DNSChef is shutting down. 322 | ``` 323 | 324 | ## Other Configurations 325 | 326 | For security reasons, DNSChef listens on a local 127.0.0.1 (or ::1 for IPv6) interface by default. You can make DNSChef listen on another interface using the --interface parameter: 327 | ``` 328 | # ./dnschef.py --interface 0.0.0.0 -q 329 | [*] DNSChef started on interface: 0.0.0.0 330 | [*] Using the following nameservers: 8.8.8.8 331 | [*] No parameters were specified. Running in full proxy mode 332 | [00:50:53] 192.0.2.105: proxying the response of type 'A' for thesprawl.org 333 | ``` 334 | 335 | or for IPv6: 336 | ``` 337 | # ./dnschef.py -6 --interface :: -q 338 | [*] Using IPv6 mode. 339 | [*] DNSChef started on interface: :: 340 | [*] Using the following nameservers: 2001:4860:4860::8888 341 | [*] No parameters were specified. Running in full proxy mode 342 | [00:57:46] 2001:db8::105: proxying the response of type 'A' for thesprawl.org 343 | ``` 344 | 345 | By default, DNSChef uses Google's public DNS server to make proxy requests. However, you can define a custom list of nameservers using the --nameservers parameter: 346 | ``` 347 | # ./dnschef.py --nameservers 4.2.2.1,4.2.2.2 -q 348 | [*] DNSChef started on interface: 127.0.0.1 349 | [*] Using the following nameservers: 4.2.2.1, 4.2.2.2 350 | [*] No parameters were specified. Running in full proxy mode 351 | [00:55:08] 127.0.0.1: proxying the response of type 'A' for thesprawl.org 352 | ``` 353 | 354 | It is possible to specify non-standard nameserver port using IP#PORT notation: 355 | ``` 356 | # ./dnschef.py --nameservers 192.0.2.2#5353 -q 357 | [*] DNSChef started on interface: 127.0.0.1 358 | [*] Using the following nameservers: 192.0.2.2#5353 359 | [*] No parameters were specified. Running in full proxy mode 360 | [02:03:12] 127.0.0.1: proxying the response of type 'A' for thesprawl.org 361 | ``` 362 | 363 | By default, DNSChef will connect to remote nameservers using UDP protocol. This behavior can be controlled more precisely using IP#PORT#PROTOCOL notation to use TCP protocol instead: 364 | ``` 365 | # ./dnschef.py --nameservers 4.2.2.2#53#tcp -q 366 | [*] DNSChef started on interface: 127.0.0.1 367 | [*] Using the following nameservers: 4.2.2.2#53#tcp 368 | [*] No parameters were specified. Running in full proxy mode 369 | [22:08:48] 127.0.0.1: proxying the response of type 'A' for thesprawl.org 370 | ``` 371 | 372 | At the same time it is possible to start DNSChef itself on an alternative port using the -p port# parameter: 373 | ``` 374 | # ./dnschef.py -p 5353 -q 375 | [*] Listening on an alternative port 5353 376 | [*] DNSChef started on interface: 127.0.0.1 377 | [*] Using the following nameservers: 8.8.8.8 378 | [*] No parameters were specified. Running in full proxy mode 379 | ``` 380 | 381 | DNS protocol can be used over UDP (default) or TCP. DNSChef implements a TCP mode which can be activated with the --tcp flag: 382 | ``` 383 | # ./dnschef.py --tcp -q 384 | [*] DNSChef started on interface: 127.0.0.1 385 | [*] Using the following nameservers: 8.8.8.8 386 | [*] No parameters were specified. Running in full proxy mode 387 | [*] DNSChef is running in TCP mode 388 | ``` 389 | 390 | ## Internal Architecture 391 | Here is some information on the internals in case you need to adapt the tool for your needs. DNSChef is built on top of the SocketServer module and uses threading to help process multiple requests simultaneously. The tool is designed to listen on TCP or UDP ports (default is port 53) for incoming requests and forward those requests when necessary to a real DNS server over UDP. 392 | 393 | The excellent dnslib library is used to dissect and reassemble DNS packets. It is particularly useful when generating response packets based on queries. IPy is used for IPv6 addresses manipulation. Both libraries come bundled with DNSChef to ease installation. 394 | 395 | DNSChef is capable of modifing queries for records of type "A", "AAAA", "MX", "CNAME", "NS", "TXT", "PTR", "NAPTR", "SOA", "SRV", "DNSKEY", "RRSIG", "ANY". It is very easy to expand or modify behavior for any record. Simply add another if qtype *== "RECORD TYPE"* entry and tell it what to reply with. 396 | 397 | Enjoy the tool and forward all requests and comments to iphelix [at] thesprawl.org. 398 | 399 | Happy proxying! -------------------------------------------------------------------------------- /dnschef.ini: -------------------------------------------------------------------------------- 1 | [A] # Queries for IPv4 address records 2 | *.thesprawl.org=192.0.2.1 3 | 4 | [AAAA] # Queries for IPv6 address records 5 | *.thesprawl.org=2001:db8::1 6 | 7 | [MX] # Queries for mail server records 8 | *.thesprawl.org=mail.fake.com 9 | 10 | [NS] # Queries for mail server records 11 | *.thesprawl.org=ns.fake.com 12 | 13 | [CNAME] # Queries for alias records 14 | *.thesprawl.org=www.fake.com 15 | 16 | [TXT] # Queries for text records 17 | *.thesprawl.org=fake message 18 | 19 | [PTR] 20 | *.2.0.192.in-addr.arpa=fake.com 21 | 22 | [SOA] 23 | ; FORMAT: mname rname t1 t2 t3 t4 t5 24 | *.thesprawl.org=ns.fake.com. hostmaster.fake.com. 1 10800 3600 604800 3600 25 | 26 | [NAPTR] 27 | ; FORMAT: order preference flags service regexp replacement 28 | *.thesprawl.org=100 10 U E2U+sip !^.*$!sip:customer-service@fake.com! . 29 | 30 | [SRV] 31 | ; FORMAT: priority weight port target 32 | *.*.thesprawl.org=0 5 5060 sipserver.fake.com 33 | 34 | [DNSKEY] 35 | ; FORMAT: flags protocol algorithm base64(key) 36 | *.thesprawl.org=256 3 5 AQPSKmynfzW4kyBv015MUG2DeIQ3Cbl+BBZH4b/0PY1kxkmvHjcZc8nokfzj31GajIQKY+5CptLr3buXA10hWqTkF7H6RfoRqXQeogmMHfpftf6zMv1LyBUgia7za6ZEzOJBOztyvhjL742iU/TpPSEDhm2SNKLijfUppn1UaNvv4w== 37 | 38 | [RRSIG] 39 | ; FORMAT: covered algorithm labels labels orig_ttl sig_exp sig_inc key_tag name base64(sig) 40 | *.thesprawl.org=A 5 3 86400 20030322173103 20030220173103 2642 thesprawl.org. oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6oB9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkGJ5D6fwFm8nN+6pBzeDQfsS3Ap3o= -------------------------------------------------------------------------------- /dnschef.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # DNSChef is a highly configurable DNS Proxy for Penetration Testers 4 | # and Malware Analysts. Please visit http://thesprawl.org/projects/dnschef/ 5 | # for the latest version and documentation. Please forward all issues and 6 | # concerns to iphelix [at] thesprawl.org. 7 | 8 | DNSCHEF_VERSION = "0.3" 9 | 10 | # Copyright (C) 2014 Peter Kacherginsky 11 | # All rights reserved. 12 | # 13 | # Redistribution and use in source and binary forms, with or without 14 | # modification, are permitted provided that the following conditions are met: 15 | # 16 | # 1. Redistributions of source code must retain the above copyright notice, this 17 | # list of conditions and the following disclaimer. 18 | # 2. Redistributions in binary form must reproduce the above copyright notice, 19 | # this list of conditions and the following disclaimer in the documentation 20 | # and/or other materials provided with the distribution. 21 | # 3. Neither the name of the copyright holder nor the names of its contributors 22 | # may be used to endorse or promote products derived from this software without 23 | # specific prior written permission. 24 | # 25 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 32 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | 36 | from optparse import OptionParser,OptionGroup 37 | from ConfigParser import ConfigParser 38 | 39 | from dnslib import * 40 | from IPy import IP 41 | 42 | import threading, random, operator, time 43 | import SocketServer, socket, sys, os 44 | import binascii 45 | import string 46 | import base64 47 | import time 48 | 49 | # DNSHandler Mixin. The class contains generic functions to parse DNS requests and 50 | # calculate an appropriate response based on user parameters. 51 | class DNSHandler(): 52 | 53 | def parse(self,data): 54 | response = "" 55 | 56 | try: 57 | # Parse data as DNS 58 | d = DNSRecord.parse(data) 59 | 60 | except Exception, e: 61 | print "[%s] %s: ERROR: %s" % (time.strftime("%H:%M:%S"), self.client_address[0], "invalid DNS request") 62 | if self.server.log: self.server.log.write("[%s] %s: ERROR: %s\n" % (time.strftime("%d/%b/%Y:%H:%M:%S %z"), self.client_address[0], "invalid DNS request")) 63 | 64 | else: 65 | # Only Process DNS Queries 66 | if QR[d.header.qr] == "QUERY": 67 | 68 | # Gather query parameters 69 | # NOTE: Do not lowercase qname here, because we want to see 70 | # any case request weirdness in the logs. 71 | qname = str(d.q.qname) 72 | 73 | # Chop off the last period 74 | if qname[-1] == '.': qname = qname[:-1] 75 | 76 | qtype = QTYPE[d.q.qtype] 77 | 78 | # Find all matching fake DNS records for the query name or get False 79 | fake_records = dict() 80 | 81 | for record in self.server.nametodns: 82 | 83 | fake_records[record] = self.findnametodns(qname,self.server.nametodns[record]) 84 | 85 | # Check if there is a fake record for the current request qtype 86 | if qtype in fake_records and fake_records[qtype]: 87 | 88 | fake_record = fake_records[qtype] 89 | 90 | # Create a custom response to the query 91 | response = DNSRecord(DNSHeader(id=d.header.id, bitmap=d.header.bitmap, qr=1, aa=1, ra=1), q=d.q) 92 | 93 | print "[%s] %s: cooking the response of type '%s' for %s to %s" % (time.strftime("%H:%M:%S"), self.client_address[0], qtype, qname, fake_record) 94 | if self.server.log: self.server.log.write( "[%s] %s: cooking the response of type '%s' for %s to %s\n" % (time.strftime("%d/%b/%Y:%H:%M:%S %z"), self.client_address[0], qtype, qname, fake_record) ) 95 | 96 | # IPv6 needs additional work before inclusion: 97 | if qtype == "AAAA": 98 | ipv6 = IP(fake_record) 99 | ipv6_bin = ipv6.strBin() 100 | ipv6_hex_tuple = [int(ipv6_bin[i:i+8],2) for i in xrange(0,len(ipv6_bin),8)] 101 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](ipv6_hex_tuple))) 102 | 103 | elif qtype == "SOA": 104 | mname,rname,t1,t2,t3,t4,t5 = fake_record.split(" ") 105 | times = tuple([int(t) for t in [t1,t2,t3,t4,t5]]) 106 | 107 | # dnslib doesn't like trailing dots 108 | if mname[-1] == ".": mname = mname[:-1] 109 | if rname[-1] == ".": rname = rname[:-1] 110 | 111 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](mname,rname,times))) 112 | 113 | elif qtype == "NAPTR": 114 | order,preference,flags,service,regexp,replacement = fake_record.split(" ") 115 | order = int(order) 116 | preference = int(preference) 117 | 118 | # dnslib doesn't like trailing dots 119 | if replacement[-1] == ".": replacement = replacement[:-1] 120 | 121 | response.add_answer( RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](order,preference,flags,service,regexp,DNSLabel(replacement))) ) 122 | 123 | elif qtype == "SRV": 124 | priority, weight, port, target = fake_record.split(" ") 125 | priority = int(priority) 126 | weight = int(weight) 127 | port = int(port) 128 | if target[-1] == ".": target = target[:-1] 129 | 130 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](priority, weight, port, target) )) 131 | 132 | elif qtype == "DNSKEY": 133 | flags, protocol, algorithm, key = fake_record.split(" ") 134 | flags = int(flags) 135 | protocol = int(protocol) 136 | algorithm = int(algorithm) 137 | key = base64.b64decode(("".join(key)).encode('ascii')) 138 | 139 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](flags, protocol, algorithm, key) )) 140 | 141 | elif qtype == "RRSIG": 142 | covered, algorithm, labels, orig_ttl, sig_exp, sig_inc, key_tag, name, sig = fake_record.split(" ") 143 | covered = getattr(QTYPE,covered) # NOTE: Covered QTYPE 144 | algorithm = int(algorithm) 145 | labels = int(labels) 146 | orig_ttl = int(orig_ttl) 147 | sig_exp = int(time.mktime(time.strptime(sig_exp +'GMT',"%Y%m%d%H%M%S%Z"))) 148 | sig_inc = int(time.mktime(time.strptime(sig_inc +'GMT',"%Y%m%d%H%M%S%Z"))) 149 | key_tag = int(key_tag) 150 | if name[-1] == '.': name = name[:-1] 151 | sig = base64.b64decode(("".join(sig)).encode('ascii')) 152 | 153 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](covered, algorithm, labels,orig_ttl, sig_exp, sig_inc, key_tag, name, sig) )) 154 | 155 | else: 156 | # dnslib doesn't like trailing dots 157 | if fake_record[-1] == ".": fake_record = fake_record[:-1] 158 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](fake_record))) 159 | 160 | response = response.pack() 161 | 162 | elif qtype == "*" and not None in fake_records.values(): 163 | print "[%s] %s: cooking the response of type '%s' for %s with %s" % (time.strftime("%H:%M:%S"), self.client_address[0], "ANY", qname, "all known fake records.") 164 | if self.server.log: self.server.log.write( "[%s] %s: cooking the response of type '%s' for %s with %s\n" % (time.strftime("%d/%b/%Y:%H:%M:%S %z"), self.client_address[0], "ANY", qname, "all known fake records.") ) 165 | 166 | response = DNSRecord(DNSHeader(id=d.header.id, bitmap=d.header.bitmap,qr=1, aa=1, ra=1), q=d.q) 167 | 168 | for qtype,fake_record in fake_records.items(): 169 | if fake_record: 170 | 171 | # NOTE: RDMAP is a dictionary map of qtype strings to handling classses 172 | # IPv6 needs additional work before inclusion: 173 | if qtype == "AAAA": 174 | ipv6 = IP(fake_record) 175 | ipv6_bin = ipv6.strBin() 176 | fake_record = [int(ipv6_bin[i:i+8],2) for i in xrange(0,len(ipv6_bin),8)] 177 | 178 | elif qtype == "SOA": 179 | mname,rname,t1,t2,t3,t4,t5 = fake_record.split(" ") 180 | times = tuple([int(t) for t in [t1,t2,t3,t4,t5]]) 181 | 182 | # dnslib doesn't like trailing dots 183 | if mname[-1] == ".": mname = mname[:-1] 184 | if rname[-1] == ".": rname = rname[:-1] 185 | 186 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](mname,rname,times))) 187 | 188 | elif qtype == "NAPTR": 189 | order,preference,flags,service,regexp,replacement = fake_record.split(" ") 190 | order = int(order) 191 | preference = int(preference) 192 | 193 | # dnslib doesn't like trailing dots 194 | if replacement and replacement[-1] == ".": replacement = replacement[:-1] 195 | 196 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](order,preference,flags,service,regexp,replacement))) 197 | 198 | elif qtype == "SRV": 199 | priority, weight, port, target = fake_record.split(" ") 200 | priority = int(priority) 201 | weight = int(weight) 202 | port = int(port) 203 | if target[-1] == ".": target = target[:-1] 204 | 205 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](priority, weight, port, target) )) 206 | 207 | elif qtype == "DNSKEY": 208 | flags, protocol, algorithm, key = fake_record.split(" ") 209 | flags = int(flags) 210 | protocol = int(protocol) 211 | algorithm = int(algorithm) 212 | key = base64.b64decode(("".join(key)).encode('ascii')) 213 | 214 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](flags, protocol, algorithm, key) )) 215 | 216 | elif qtype == "RRSIG": 217 | covered, algorithm, labels, orig_ttl, sig_exp, sig_inc, key_tag, name, sig = fake_record.split(" ") 218 | covered = getattr(QTYPE,covered) # NOTE: Covered QTYPE 219 | algorithm = int(algorithm) 220 | labels = int(labels) 221 | orig_ttl = int(orig_ttl) 222 | sig_exp = int(time.mktime(time.strptime(sig_exp +'GMT',"%Y%m%d%H%M%S%Z"))) 223 | sig_inc = int(time.mktime(time.strptime(sig_inc +'GMT',"%Y%m%d%H%M%S%Z"))) 224 | key_tag = int(key_tag) 225 | if name[-1] == '.': name = name[:-1] 226 | sig = base64.b64decode(("".join(sig)).encode('ascii')) 227 | 228 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](covered, algorithm, labels,orig_ttl, sig_exp, sig_inc, key_tag, name, sig) )) 229 | 230 | else: 231 | # dnslib doesn't like trailing dots 232 | if fake_record[-1] == ".": fake_record = fake_record[:-1] 233 | response.add_answer(RR(qname, getattr(QTYPE,qtype), rdata=RDMAP[qtype](fake_record))) 234 | 235 | response = response.pack() 236 | 237 | # Proxy the request 238 | else: 239 | print "[%s] %s: proxying the response of type '%s' for %s" % (time.strftime("%H:%M:%S"), self.client_address[0], qtype, qname) 240 | if self.server.log: self.server.log.write( "[%s] %s: proxying the response of type '%s' for %s\n" % (time.strftime("%d/%b/%Y:%H:%M:%S %z"), self.client_address[0], qtype, qname) ) 241 | 242 | nameserver_tuple = random.choice(self.server.nameservers).split('#') 243 | response = self.proxyrequest(data,*nameserver_tuple) 244 | 245 | return response 246 | 247 | 248 | # Find appropriate ip address to use for a queried name. The function can 249 | def findnametodns(self,qname,nametodns): 250 | 251 | # Make qname case insensitive 252 | qname = qname.lower() 253 | 254 | # Split and reverse qname into components for matching. 255 | qnamelist = qname.split('.') 256 | qnamelist.reverse() 257 | 258 | # HACK: It is important to search the nametodns dictionary before iterating it so that 259 | # global matching ['*.*.*.*.*.*.*.*.*.*'] will match last. Use sorting for that. 260 | for domain,host in sorted(nametodns.iteritems(), key=operator.itemgetter(1)): 261 | 262 | # NOTE: It is assumed that domain name was already lowercased 263 | # when it was loaded through --file, --fakedomains or --truedomains 264 | # don't want to waste time lowercasing domains on every request. 265 | 266 | # Split and reverse domain into components for matching 267 | domain = domain.split('.') 268 | domain.reverse() 269 | 270 | # Compare domains in reverse. 271 | for a,b in map(None,qnamelist,domain): 272 | if a != b and b != "*": 273 | break 274 | else: 275 | # Could be a real IP or False if we are doing reverse matching with 'truedomains' 276 | return host 277 | else: 278 | return False 279 | 280 | # Obtain a response from a real DNS server. 281 | def proxyrequest(self, request, host, port="53", protocol="udp"): 282 | reply = None 283 | try: 284 | if self.server.ipv6: 285 | 286 | if protocol == "udp": 287 | sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 288 | elif protocol == "tcp": 289 | sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 290 | 291 | else: 292 | if protocol == "udp": 293 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 294 | elif protocol == "tcp": 295 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 296 | 297 | sock.settimeout(3.0) 298 | 299 | # Send the proxy request to a randomly chosen DNS server 300 | 301 | if protocol == "udp": 302 | sock.sendto(request, (host, int(port))) 303 | reply = sock.recv(1024) 304 | sock.close() 305 | 306 | elif protocol == "tcp": 307 | sock.connect((host, int(port))) 308 | 309 | # Add length for the TCP request 310 | length = binascii.unhexlify("%04x" % len(request)) 311 | sock.sendall(length+request) 312 | 313 | # Strip length from the response 314 | reply = sock.recv(1024) 315 | reply = reply[2:] 316 | 317 | sock.close() 318 | 319 | except Exception, e: 320 | print "[!] Could not proxy request: %s" % e 321 | else: 322 | return reply 323 | 324 | # UDP DNS Handler for incoming requests 325 | class UDPHandler(DNSHandler, SocketServer.BaseRequestHandler): 326 | 327 | def handle(self): 328 | (data,socket) = self.request 329 | response = self.parse(data) 330 | 331 | if response: 332 | socket.sendto(response, self.client_address) 333 | 334 | # TCP DNS Handler for incoming requests 335 | class TCPHandler(DNSHandler, SocketServer.BaseRequestHandler): 336 | 337 | def handle(self): 338 | data = self.request.recv(1024) 339 | 340 | # Remove the addition "length" parameter used in the 341 | # TCP DNS protocol 342 | data = data[2:] 343 | response = self.parse(data) 344 | 345 | if response: 346 | # Calculate and add the additional "length" parameter 347 | # used in TCP DNS protocol 348 | length = binascii.unhexlify("%04x" % len(response)) 349 | self.request.sendall(length+response) 350 | 351 | class ThreadedUDPServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer): 352 | 353 | # Override SocketServer.UDPServer to add extra parameters 354 | def __init__(self, server_address, RequestHandlerClass, nametodns, nameservers, ipv6, log): 355 | self.nametodns = nametodns 356 | self.nameservers = nameservers 357 | self.ipv6 = ipv6 358 | self.address_family = socket.AF_INET6 if self.ipv6 else socket.AF_INET 359 | self.log = log 360 | 361 | SocketServer.UDPServer.__init__(self,server_address,RequestHandlerClass) 362 | 363 | class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): 364 | 365 | # Override default value 366 | allow_reuse_address = True 367 | 368 | # Override SocketServer.TCPServer to add extra parameters 369 | def __init__(self, server_address, RequestHandlerClass, nametodns, nameservers, ipv6, log): 370 | self.nametodns = nametodns 371 | self.nameservers = nameservers 372 | self.ipv6 = ipv6 373 | self.address_family = socket.AF_INET6 if self.ipv6 else socket.AF_INET 374 | self.log = log 375 | 376 | SocketServer.TCPServer.__init__(self,server_address,RequestHandlerClass) 377 | 378 | # Initialize and start the DNS Server 379 | def start_cooking(interface, nametodns, nameservers, tcp=False, ipv6=False, port="53", logfile=None): 380 | try: 381 | 382 | if logfile: 383 | log = open(logfile,'a',0) 384 | log.write("[%s] DNSChef is active.\n" % (time.strftime("%d/%b/%Y:%H:%M:%S %z")) ) 385 | else: 386 | log = None 387 | 388 | if tcp: 389 | print "[*] DNSChef is running in TCP mode" 390 | server = ThreadedTCPServer((interface, int(port)), TCPHandler, nametodns, nameservers, ipv6, log) 391 | else: 392 | server = ThreadedUDPServer((interface, int(port)), UDPHandler, nametodns, nameservers, ipv6, log) 393 | 394 | # Start a thread with the server -- that thread will then start 395 | # more threads for each request 396 | server_thread = threading.Thread(target=server.serve_forever) 397 | 398 | # Exit the server thread when the main thread terminates 399 | server_thread.daemon = True 400 | server_thread.start() 401 | 402 | # Loop in the main thread 403 | while True: time.sleep(100) 404 | 405 | except (KeyboardInterrupt, SystemExit): 406 | 407 | if log: 408 | log.write("[%s] DNSChef is shutting down.\n" % (time.strftime("%d/%b/%Y:%H:%M:%S %z")) ) 409 | log.close() 410 | 411 | server.shutdown() 412 | print "[*] DNSChef is shutting down." 413 | sys.exit() 414 | 415 | except IOError: 416 | print "[!] Failed to open log file for writing." 417 | 418 | except Exception, e: 419 | print "[!] Failed to start the server: %s" % e 420 | 421 | if __name__ == "__main__": 422 | 423 | header = " _ _ __ \n" 424 | header += " | | version %s | | / _| \n" % DNSCHEF_VERSION 425 | header += " __| |_ __ ___ ___| |__ ___| |_ \n" 426 | header += " / _` | '_ \/ __|/ __| '_ \ / _ \ _|\n" 427 | header += " | (_| | | | \__ \ (__| | | | __/ | \n" 428 | header += " \__,_|_| |_|___/\___|_| |_|\___|_| \n" 429 | header += " iphelix@thesprawl.org \n" 430 | 431 | # Parse command line arguments 432 | parser = OptionParser(usage = "dnschef.py [options]:\n" + header, description="DNSChef is a highly configurable DNS Proxy for Penetration Testers and Malware Analysts. It is capable of fine configuration of which DNS replies to modify or to simply proxy with real responses. In order to take advantage of the tool you must either manually configure or poison DNS server entry to point to DNSChef. The tool requires root privileges to run on privileged ports." ) 433 | 434 | fakegroup = OptionGroup(parser, "Fake DNS records:") 435 | fakegroup.add_option('--fakeip', metavar="192.0.2.1", action="store", help='IP address to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'A\' queries will be spoofed. Consider using --file argument if you need to define more than one IP address.') 436 | fakegroup.add_option('--fakeipv6', metavar="2001:db8::1", action="store", help='IPv6 address to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'AAAA\' queries will be spoofed. Consider using --file argument if you need to define more than one IPv6 address.') 437 | fakegroup.add_option('--fakemail', metavar="mail.fake.com", action="store", help='MX name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'MX\' queries will be spoofed. Consider using --file argument if you need to define more than one MX record.') 438 | fakegroup.add_option('--fakealias', metavar="www.fake.com", action="store", help='CNAME name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'CNAME\' queries will be spoofed. Consider using --file argument if you need to define more than one CNAME record.') 439 | fakegroup.add_option('--fakens', metavar="ns.fake.com", action="store", help='NS name to use for matching DNS queries. If you use this parameter without specifying domain names, then all \'NS\' queries will be spoofed. Consider using --file argument if you need to define more than one NS record.') 440 | fakegroup.add_option('--file', action="store", help="Specify a file containing a list of DOMAIN=IP pairs (one pair per line) used for DNS responses. For example: google.com=1.1.1.1 will force all queries to 'google.com' to be resolved to '1.1.1.1'. IPv6 addresses will be automatically detected. You can be even more specific by combining --file with other arguments. However, data obtained from the file will take precedence over others.") 441 | parser.add_option_group(fakegroup) 442 | 443 | parser.add_option('--fakedomains', metavar="thesprawl.org,google.com", action="store", help='A comma separated list of domain names which will be resolved to FAKE values specified in the the above parameters. All other domain names will be resolved to their true values.') 444 | parser.add_option('--truedomains', metavar="thesprawl.org,google.com", action="store", help='A comma separated list of domain names which will be resolved to their TRUE values. All other domain names will be resolved to fake values specified in the above parameters.') 445 | 446 | rungroup = OptionGroup(parser,"Optional runtime parameters.") 447 | rungroup.add_option("--logfile", action="store", help="Specify a log file to record all activity") 448 | rungroup.add_option("--nameservers", metavar="8.8.8.8#53 or 4.2.2.1#53#tcp or 2001:4860:4860::8888", default='8.8.8.8', action="store", help='A comma separated list of alternative DNS servers to use with proxied requests. Nameservers can have either IP or IP#PORT format. A randomly selected server from the list will be used for proxy requests when provided with multiple servers. By default, the tool uses Google\'s public DNS server 8.8.8.8 when running in IPv4 mode and 2001:4860:4860::8888 when running in IPv6 mode.') 449 | rungroup.add_option("-i","--interface", metavar="127.0.0.1 or ::1", default="127.0.0.1", action="store", help='Define an interface to use for the DNS listener. By default, the tool uses 127.0.0.1 for IPv4 mode and ::1 for IPv6 mode.') 450 | rungroup.add_option("-t","--tcp", action="store_true", default=False, help="Use TCP DNS proxy instead of the default UDP.") 451 | rungroup.add_option("-6","--ipv6", action="store_true", default=False, help="Run in IPv6 mode.") 452 | rungroup.add_option("-p","--port", action="store", metavar="53", default="53", help='Port number to listen for DNS requests.') 453 | rungroup.add_option("-q", "--quiet", action="store_false", dest="verbose", default=True, help="Don't show headers.") 454 | parser.add_option_group(rungroup) 455 | 456 | (options,args) = parser.parse_args() 457 | 458 | # Print program header 459 | if options.verbose: 460 | print header 461 | 462 | # Main storage of domain filters 463 | # NOTE: RDMAP is a dictionary map of qtype strings to handling classes 464 | nametodns = dict() 465 | for qtype in RDMAP.keys(): 466 | nametodns[qtype] = dict() 467 | 468 | # Incorrect or incomplete command line arguments 469 | if options.fakedomains and options.truedomains: 470 | print "[!] You can not specify both 'fakedomains' and 'truedomains' parameters." 471 | sys.exit(0) 472 | 473 | elif not (options.fakeip or options.fakeipv6) and (options.fakedomains or options.truedomains): 474 | print "[!] You have forgotten to specify which IP to use for fake responses" 475 | sys.exit(0) 476 | 477 | # Notify user about alternative listening port 478 | if options.port != "53": 479 | print "[*] Listening on an alternative port %s" % options.port 480 | 481 | # Adjust defaults for IPv6 482 | if options.ipv6: 483 | print "[*] Using IPv6 mode." 484 | if options.interface == "127.0.0.1": 485 | options.interface = "::1" 486 | 487 | if options.nameservers == "8.8.8.8": 488 | options.nameservers = "2001:4860:4860::8888" 489 | 490 | print "[*] DNSChef started on interface: %s " % options.interface 491 | 492 | # Use alternative DNS servers 493 | if options.nameservers: 494 | nameservers = options.nameservers.split(',') 495 | print "[*] Using the following nameservers: %s" % ", ".join(nameservers) 496 | 497 | # External file definitions 498 | if options.file: 499 | config = ConfigParser() 500 | config.read(options.file) 501 | for section in config.sections(): 502 | 503 | if section in nametodns: 504 | for domain,record in config.items(section): 505 | 506 | # Make domain case insensitive 507 | domain = domain.lower() 508 | 509 | nametodns[section][domain] = record 510 | print "[+] Cooking %s replies for domain %s with '%s'" % (section,domain,record) 511 | else: 512 | print "[!] DNS Record '%s' is not supported. Ignoring section contents." % section 513 | 514 | # DNS Record and Domain Name definitions 515 | # NOTE: '*.*.*.*.*.*.*.*.*.*' domain is used to match all possible queries. 516 | if options.fakeip or options.fakeipv6 or options.fakemail or options.fakealias or options.fakens: 517 | fakeip = options.fakeip 518 | fakeipv6 = options.fakeipv6 519 | fakemail = options.fakemail 520 | fakealias = options.fakealias 521 | fakens = options.fakens 522 | 523 | if options.fakedomains: 524 | for domain in options.fakedomains.split(','): 525 | 526 | # Make domain case insensitive 527 | domain = domain.lower() 528 | domain = domain.strip() 529 | 530 | if fakeip: 531 | nametodns["A"][domain] = fakeip 532 | print "[*] Cooking A replies to point to %s matching: %s" % (options.fakeip, domain) 533 | 534 | if fakeipv6: 535 | nametodns["AAAA"][domain] = fakeipv6 536 | print "[*] Cooking AAAA replies to point to %s matching: %s" % (options.fakeipv6, domain) 537 | 538 | if fakemail: 539 | nametodns["MX"][domain] = fakemail 540 | print "[*] Cooking MX replies to point to %s matching: %s" % (options.fakemail, domain) 541 | 542 | if fakealias: 543 | nametodns["CNAME"][domain] = fakealias 544 | print "[*] Cooking CNAME replies to point to %s matching: %s" % (options.fakealias, domain) 545 | 546 | if fakens: 547 | nametodns["NS"][domain] = fakens 548 | print "[*] Cooking NS replies to point to %s matching: %s" % (options.fakens, domain) 549 | 550 | elif options.truedomains: 551 | for domain in options.truedomains.split(','): 552 | 553 | # Make domain case insensitive 554 | domain = domain.lower() 555 | domain = domain.strip() 556 | 557 | if fakeip: 558 | nametodns["A"][domain] = False 559 | print "[*] Cooking A replies to point to %s not matching: %s" % (options.fakeip, domain) 560 | nametodns["A"]['*.*.*.*.*.*.*.*.*.*'] = fakeip 561 | 562 | if fakeipv6: 563 | nametodns["AAAA"][domain] = False 564 | print "[*] Cooking AAAA replies to point to %s not matching: %s" % (options.fakeipv6, domain) 565 | nametodns["AAAA"]['*.*.*.*.*.*.*.*.*.*'] = fakeipv6 566 | 567 | if fakemail: 568 | nametodns["MX"][domain] = False 569 | print "[*] Cooking MX replies to point to %s not matching: %s" % (options.fakemail, domain) 570 | nametodns["MX"]['*.*.*.*.*.*.*.*.*.*'] = fakemail 571 | 572 | if fakealias: 573 | nametodns["CNAME"][domain] = False 574 | print "[*] Cooking CNAME replies to point to %s not matching: %s" % (options.fakealias, domain) 575 | nametodns["CNAME"]['*.*.*.*.*.*.*.*.*.*'] = fakealias 576 | 577 | if fakens: 578 | nametodns["NS"][domain] = False 579 | print "[*] Cooking NS replies to point to %s not matching: %s" % (options.fakens, domain) 580 | nametodns["NS"]['*.*.*.*.*.*.*.*.*.*'] = fakealias 581 | 582 | else: 583 | 584 | # NOTE: '*.*.*.*.*.*.*.*.*.*' domain is a special ANY domain 585 | # which is compatible with the wildflag algorithm above. 586 | 587 | if fakeip: 588 | nametodns["A"]['*.*.*.*.*.*.*.*.*.*'] = fakeip 589 | print "[*] Cooking all A replies to point to %s" % fakeip 590 | 591 | if fakeipv6: 592 | nametodns["AAAA"]['*.*.*.*.*.*.*.*.*.*'] = fakeipv6 593 | print "[*] Cooking all AAAA replies to point to %s" % fakeipv6 594 | 595 | if fakemail: 596 | nametodns["MX"]['*.*.*.*.*.*.*.*.*.*'] = fakemail 597 | print "[*] Cooking all MX replies to point to %s" % fakemail 598 | 599 | if fakealias: 600 | nametodns["CNAME"]['*.*.*.*.*.*.*.*.*.*'] = fakealias 601 | print "[*] Cooking all CNAME replies to point to %s" % fakealias 602 | 603 | if fakens: 604 | nametodns["NS"]['*.*.*.*.*.*.*.*.*.*'] = fakens 605 | print "[*] Cooking all NS replies to point to %s" % fakens 606 | 607 | # Proxy all DNS requests 608 | if not options.fakeip and not options.fakeipv6 and not options.fakemail and not options.fakealias and not options.fakens and not options.file: 609 | print "[*] No parameters were specified. Running in full proxy mode" 610 | 611 | # Launch DNSChef 612 | start_cooking(interface=options.interface, nametodns=nametodns, nameservers=nameservers, tcp=options.tcp, ipv6=options.ipv6, port=options.port, logfile=options.logfile) --------------------------------------------------------------------------------