A simple, regex-ready and scriptable authoritative DNS server for toying, testing and red teaming.
Features:
- Almost fully compatible with standard BIND zone files as defined in RFC 1035.
- Regex based dynamic record matching.
- Dynamic responses using Lua scripts (ala PowerDNS) thanks to the awesome MoonSharp Lua interpreter.
- Easily extensible. Currently supports A, AAAA, CNAME, NS, MX, TXT, PTR, SOA and NULL records.
- Easy to deploy.
Anti-features 🙂:
- No zone transfers.
- No DNSSEC.
- No recursive queries.
To-do:
- More robust networking code.
- Add domain compression to responses.
- Upgrade Lua engine to a recent version of MoonSharp.
Written by Luis Medel, Percibe Information Security.
Copyright (c) 2021, Percibe S.L. All rights reserved.
This program is released under a 3-clause BSD license. For more detail see the LICENSE.md file.
redns [options]
Where options can be:
--zone <path> Loads zone from file (defaults to ./zone.conf)
(can be used multiple times)
--no-reverse-zone Don't add a reverse zone.
--bind <proto>:<addr>:<port> Listens at the specified endpoint, where:
<proto> udp|tcp.
<addr> local address to bind to.
<port> local port to bind to.
(can be used multiple times)
--log <path> Send logs to file.
--no-console Disables console output.
--loglevel <level> debug|info|warn|error|all (defaults to debug).
--help Prints this screen and exit.
Call redns without arguments to start the default server.
redns --zone .\zone.conf \
--bind udp:0.0.0.0:53 \
--bind tcp:0.0.0.0:53 \
--loglevel debug
redns tries to be fully compatible with standard BIND zone files. Nevertheless, some syntax changes were neccesary to add suport for regex defined records and Lua scripts.
You can feed redns with any standard zone file. For example:
$ORIGIN example.com. ; Designates the start of this zone file
$TTL 3600 ; Default expiration time (in seconds) of all RRs
; without their own TTL value
example.com. IN SOA ns.example.com. admin.example.com. ( 2020091025
7200
3600
1209600
3600 )
@ IN NS ns ; A nameserver for example.com
@ IN NS ns2 ; Another nameserver
ns IN A 192.0.2.2 ; IPv4 for ns.example.com
ns2 IN A 192.0.2.3 ; IPv4 for ns2.example.com
example.com. IN A 192.0.2.1 ; IPv4 address for example.com
@ IN AAAA 2001:db8:10::1 ; IPv6 address for example.com
www IN CNAME example.com. ; An alias for example.com
wwwtest IN CNAME www ; Another alias for www.example.com
mail IN A 192.0.2.3 ; IPv4 address for mail.example.com
mail2 IN A 192.0.2.4 ; IPv4 address for mail2.example.com
mail3 IN A 192.0.2.5 ; IPv4 address for mail3.example.com
This is where the magic of redns happens.
redns supports regex based dynamic record matching. Simply define your record as usual, but using a regex pattern.
The next record matches any query starting with 'info' followed by digits (ie: info1.example.com, info999.example.com, info123456.example.com, info314159.example.com, etc.)
/^info\d+/ IN A 192.0.2.6
Note you can use any special char and anchor in your regex. Simply be aware that at runtime this:
/^info\d+/will be expanded using the zone origin. So, anchors like $ (end of text) won't be valid.
In our example:
/^info\d+\.example\.com/
You can return a custom response using Lua scripts. We use a (in our humble opinion) saner syntax than PowerDNS' one. This syntax allows to add several scripting engines to the server (ie: <?js, <?shell, etc.):
Let's return an boring IPv4 for an A record.
subdomain IN A <?lua
return "192.168.2.7"
?>
You can return more than one value, if allowed by the record type.
subdomain IN NS <?lua
return "192.0.2.2", "192.0.2.3"
?>
If the record type expects more than one value (like in MX records, for example) you can use a table. Only remember to use the same order you would use in a zone file:
example.com. IN MX <?lua
-- MX => priority, hostname
return { 10, "mail.example.com." }
?>
A more complete MX output for example.com could be this:
example.com. IN MX <?lua
return { 10, "mail.example.com." },
{ 20, "mail2.example.com." },
{ 30, "mai3.example.com." }
?>
You can alter the response type within Lua (not all DNS clients support this, though). For example, lets change from A to TXT:
subdomain2 IN A <?lua
responseType = "TXT"
return "I was an A record, but I switched to TXT!"
?>
FYI, there are 4 globals available for Lua scripts to use:
remoteAddress: Client address (read only string) (e.g.: "192.168.2.4") remotePort: Remote port (read only int) requestName: Request (read only string) (e.g.: "mail.example.com") responseType: Response type (read/write string) ("A", "AAAA", "NS", "MX", etc.) Setted by default to the apporpiate response type in each call.
Of course, you can combine both, dynamic matching and Lua scripting:
/^custom\d+/ IN TXT <?lua
return "Custom TXT for " ..
remoteAddress .. ":" ..
remotePort .. " = " .. requestName
?>
Another one. Set a catch-all record and do whatever you want with the data:
/^.+/ IN TXT <?lua
local path = "/tmp/" .. remoteAddress .. ".txt"
local file = assert (io.open (path, "w"))
file:write (requestName)
file:close ()
return "Your request was saved to '/tmp/" .. remoteAddress .. ".txt"
?>
And an usage example:
# dig @127.0.0.1 -p 5553 TXT save-this-text.example.com
; <<>> DiG 9.16.1-Ubuntu <<>> @127.0.0.1 -p 5553 TXT save-this-text.example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9964
;; flags: qr aa rd ad; QUERY: 0, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; ANSWER SECTION:
save-this-text.example.com. 0 IN TXT "Your request was saved to '/tmp/127.0.0.1.txt"
;; Query time: 27 msec
;; SERVER: 127.0.0.1#5553(127.0.0.1)
;; WHEN: vie ene 28 11:30:00 CET 2022
;; MSG SIZE rcvd: 96
This project is licensed under the Free Software Foundation's GNU AGPL v3.0.
If use of this project under the AGPL v3.0 does not satisfy your organization’s legal department, commercial licenses are available. Feel free to contact the author for more details.