# frozen_string_literal: true # # ronin-listener-dns - A DNS server for receiving exfiltrated data. # # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) # # ronin-listener-dns is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-listener-dns is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-listener-dns. If not, see . # require 'ronin/listener/dns/query' require 'async/dns' module Ronin module Listener module DNS # # A simple DNS server for receiving exfiltrated DNS queries. # class Server < Async::DNS::Server # The domain to accept queries for. # # @return [String] attr_reader :domain # The host the server will listen on. # # @return [String] attr_reader :host # The port the server will listen on. # # @return [Integer] attr_reader :port # The callback which will be passed all received queries. # # @return [Proc] # # @api private attr_reader :callback # # Initializes the DNS listener server. # # @param [String, Regexp] domain # The domain to accept queries for (ex: `example.com`). # # @param [String] host # The interface to listen on. # # @param [Integer] port # The local port to listen on. # # @yield [query] # The given block will be passed each received query. # # @yieldparam [Query] query # The received DNS query object. # # @raise [ArgumentError] # No callback block was given. # def initialize(domain, host: '0.0.0.0', port: 53, &callback) unless callback raise(ArgumentError,"#{self.class}#initialize requires a callback block") end @domain = domain @suffix = ".#{domain}" @host = host @port = port super([[:udp, host, port]]) @callback = callback end # Mapping of Resolv resource classes to Symbols. # # @api private RECORD_TYPES = { Resolv::DNS::Resource::IN::A => :A, Resolv::DNS::Resource::IN::AAAA => :AAAA, Resolv::DNS::Resource::IN::ANY => :ANY, Resolv::DNS::Resource::IN::CNAME => :CNAME, Resolv::DNS::Resource::IN::HINFO => :HINFO, Resolv::DNS::Resource::IN::LOC => :LOC, Resolv::DNS::Resource::IN::MINFO => :MINFO, Resolv::DNS::Resource::IN::MX => :MX, Resolv::DNS::Resource::IN::NS => :NS, Resolv::DNS::Resource::IN::PTR => :PTR, Resolv::DNS::Resource::IN::SOA => :SOA, Resolv::DNS::Resource::IN::SRV => :SRV, Resolv::DNS::Resource::IN::TXT => :TXT, Resolv::DNS::Resource::IN::WKS => :WKS } # # Processes an incoming query. # # @param [String] label # The queried domain label (ex: `www.example.com`). # # @param [Class] resource_class # The resource class (ex: `Resolv::DNS::Resource::IN::A`). # # @param [Async::DNS::Transaction] transaction # The DNS transaction object. # # @api private # def process(label,resource_class,transaction) # filter out queries for all other domains if label.end_with?(@suffix) # map the `Resolv::DNS::Resource::IN` class to a Symbol query_type = RECORD_TYPES.fetch(resource_class) # extract the remote address source_addr = transaction.options[:remote_address] @callback.call(Query.new(query_type,label,source_addr)) end # always respond with an error to prevent DNS caching transaction.fail!(:NXDomain) end end end end end