Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
beefproject
GitHub Repository: beefproject/beef
Path: blob/master/extensions/dns/dns.rb
1154 views
1
#
2
# Copyright (c) 2006-2025 Wade Alcorn - [email protected]
3
# Browser Exploitation Framework (BeEF) - https://beefproject.com
4
# See the file 'doc/COPYING' for copying permission
5
#
6
module BeEF
7
module Extension
8
module Dns
9
# Provides the core DNS nameserver functionality. The nameserver handles incoming requests
10
# using a rule-based system. A list of user-defined rules is used to match against incoming
11
# DNS requests. These rules generate a response that is either a resource record or a
12
# failure code.
13
class Server < Async::DNS::Server
14
include Singleton
15
16
def initialize
17
super()
18
logger.level = Logger::ERROR
19
@lock = Mutex.new
20
@database = BeEF::Core::Models::Dns::Rule
21
@data_chunks = {}
22
@server_started = false
23
end
24
25
# Adds a new DNS rule. If the rule already exists, its current ID is returned.
26
#
27
# @example Adds an A record for browserhacker.com with the IP address 1.2.3.4
28
#
29
# dns = BeEF::Extension::Dns::Server.instance
30
#
31
# id = dns.add_rule(
32
# :pattern => 'browserhacker.com',
33
# :resource => Resolv::DNS::Resource::IN::A,
34
# :response => '1.2.3.4'
35
# )
36
#
37
# @param rule [Hash] hash representation of rule
38
# @option rule [String, Regexp] :pattern match criteria
39
# @option rule [Resolv::DNS::Resource::IN] :resource resource record type
40
# @option rule [String, Array] :response server response
41
#
42
# @return [String] unique 8-digit hex identifier
43
def add_rule(rule = {})
44
@lock.synchronize do
45
# Temporarily disable warnings regarding IGNORECASE flag
46
verbose = $VERBOSE
47
$VERBOSE = nil
48
pattern = Regexp.new(rule[:pattern], Regexp::IGNORECASE)
49
$VERBOSE = verbose
50
51
@database.find_or_create_by(
52
resource: rule[:resource].to_s,
53
pattern: pattern.source,
54
response: rule[:response]
55
).id
56
end
57
end
58
59
# Retrieves a specific rule given its identifier.
60
#
61
# @param id [String] unique identifier for rule
62
#
63
# @return [Hash] hash representation of rule (empty hash if rule wasn't found)
64
def get_rule(id)
65
@lock.synchronize do
66
rule = @database.find(id)
67
return to_hash(rule)
68
rescue ActiveRecord::RecordNotFound
69
return nil
70
end
71
end
72
73
# Removes the given DNS rule.
74
#
75
# @param id [String] rule identifier
76
#
77
# @return [Boolean] true if rule was removed, otherwise false
78
def remove_rule!(id)
79
@lock.synchronize do
80
begin
81
rule = @database.find(id)
82
return true if !rule.nil? && rule.destroy
83
rescue ActiveRecord::RecordNotFound
84
return nil
85
end
86
return false
87
end
88
end
89
90
# Returns an AoH representing the entire current DNS ruleset.
91
#
92
# Each element is a hash with the following keys:
93
#
94
# * <code>:id</code>
95
# * <code>:pattern</code>
96
# * <code>:resource</code>
97
# * <code>:response</code>
98
#
99
# @return [Array<Hash>] DNS ruleset (empty array if no rules are currently defined)
100
def get_ruleset
101
@lock.synchronize { @database.all { |rule| to_hash(rule) } }
102
end
103
104
# Removes the entire DNS ruleset.
105
#
106
# @return [Boolean] true if ruleset was destroyed, otherwise false
107
def remove_ruleset!
108
@lock.synchronize do
109
return true if @database.destroy_all
110
end
111
end
112
113
# Starts the DNS server.
114
#
115
# @param options [Hash] server configuration options
116
# @option options [Array<Array>] :upstream upstream DNS servers (if ommitted, unresolvable
117
# requests return NXDOMAIN)
118
# @option options [Array<Array>] :listen local interfaces to listen on
119
def run(options = {})
120
@lock.synchronize do
121
Thread.new do
122
EventMachine.next_tick do
123
next if @server_started # Check if the server was already started
124
upstream = options[:upstream] || nil
125
126
listen = options[:listen] || nil
127
# listen is called enpoints in Async::DNS
128
@endpoints = listen
129
130
if upstream
131
resolver = Async::DNS::Resolver.new(upstream)
132
@otherwise = proc { |t| t.passthrough!(resolver) }
133
end
134
135
begin
136
# super(:listen => listen)
137
Thread.new { super() }
138
@server_started = true # Set the server started flag
139
rescue RuntimeError => e
140
if e.message =~ /no datagram socket/ || e.message =~ /no acceptor/ # the port is in use
141
print_error "[DNS] Another process is already listening on port #{options[:listen]}"
142
print_error 'Exiting...'
143
exit 127
144
else
145
raise
146
end
147
end
148
end
149
end
150
end
151
end
152
153
def stop
154
return unless @server_started # Check if the server was started
155
156
# Logic to stop the Async::DNS server
157
puts EventMachine.stop if EventMachine.reactor_running?
158
@server_started = false # Reset the server started flag
159
end
160
161
# Entry point for processing incoming DNS requests. Attempts to find a matching rule and
162
# sends back its associated response.
163
#
164
# @param name [String] name of the resource record being looked up
165
# @param resource [Resolv::DNS::Resource::IN] query type (e.g. A, CNAME, NS, etc.)
166
# @param transaction [RubyDNS::Transaction] internal RubyDNS class detailing DNS question/answer
167
def process(name, resource, transaction)
168
@lock.synchronize do
169
resource = resource.to_s
170
171
print_debug "Received DNS request (name: #{name} type: #{format_resource(resource)})"
172
173
# no need to parse AAAA resources when data is extruded from client. Also we check if the FQDN starts with the 0xb3 string.
174
# this 0xb3 is convenient to clearly separate DNS requests used to extrude data from normal DNS requests than should be resolved by the DNS server.
175
if format_resource(resource) == 'A' && name.match(/^0xb3/)
176
reconstruct(name.split('0xb3').last)
177
catch(:done) do
178
transaction.fail!(:NXDomain)
179
end
180
return
181
end
182
183
catch(:done) do
184
# Find rules matching the requested resource class
185
resources = @database.where(resource: resource)
186
throw :done if resources.length == 0
187
188
# Narrow down search by finding a matching pattern
189
resources.each do |rule|
190
pattern = Regexp.new(rule.pattern)
191
192
next unless name =~ pattern
193
194
print_debug "Found matching DNS rule (id: #{rule.id} response: #{rule.response})"
195
proc { |_t| eval(rule.callback) }.call(transaction)
196
throw :done
197
end
198
199
if @otherwise
200
print_debug 'No match found, querying upstream servers'
201
@otherwise.call(transaction)
202
else
203
print_debug 'No match found, sending NXDOMAIN response'
204
transaction.fail!(:NXDomain)
205
end
206
end
207
end
208
end
209
210
private
211
212
# Collects and reconstructs data extruded by the client and found in subdomain, with structure like:
213
# 0.1.5.4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7.browserhacker.com
214
# [...]
215
# 0.5.5.7565207175616d206469676e697373696d2065752e.browserhacker.com
216
def reconstruct(data)
217
split_data = data.split('.')
218
pack_id = split_data[0]
219
seq_num = split_data[1]
220
seq_tot = split_data[2]
221
data_chunk = split_data[3] # this might change if we store more than 63 bytes in a chunk (63 is the limitation from RFC)
222
223
unless pack_id.match(/^(\d)+$/) && seq_num.match(/^(\d)+$/) && seq_tot.match(/^(\d)+$/)
224
print_debug "[DNS] Received invalid chunk:\n #{data}"
225
return
226
end
227
228
print_debug "[DNS] Received chunk (#{seq_num} / #{seq_tot}) of packet (#{pack_id}): #{data_chunk}"
229
230
if @data_chunks[pack_id].nil?
231
# no previous chunks received, create new Array to store chunks
232
@data_chunks[pack_id] = Array.new(seq_tot.to_i)
233
@data_chunks[pack_id][seq_num.to_i - 1] = data_chunk
234
else
235
# previous chunks received, update Array
236
@data_chunks[pack_id][seq_num.to_i - 1] = data_chunk
237
if @data_chunks[pack_id].all? && @data_chunks[pack_id] != 'DONE'
238
# means that no position in the array is false/nil, so we received all the packet chunks
239
packet_data = @data_chunks[pack_id].join('')
240
decoded_packet_data = packet_data.scan(/../).map { |n| n.to_i(16) }.pack('U*')
241
print_debug "[DNS] Packet data fully received: #{packet_data}. \n Converted from HEX: #{decoded_packet_data}"
242
243
# we might get more DNS requests for the same chunks sometimes, once every chunk of a packet is received, mark it
244
@data_chunks[pack_id] = 'DONE'
245
end
246
end
247
end
248
249
# Helper method that converts a DNS rule to a hash.
250
#
251
# @param rule [BeEF::Core::Models::Dns::Rule] rule to be converted
252
#
253
# @return [Hash] hash representation of DNS rule
254
def to_hash(rule)
255
hash = {}
256
hash[:id] = rule.id
257
hash[:pattern] = rule.pattern
258
hash[:resource] = format_resource(rule.resource)
259
hash[:response] = rule.response
260
261
hash
262
end
263
264
# Verifies that the given ID is valid.
265
#
266
# @param id [String] identifier to validate
267
#
268
# @return [Boolean] true if ID is valid, otherwise false
269
def is_valid_id?(id)
270
BeEF::Filters.hexs_only?(id) &&
271
!BeEF::Filters.has_null?(id) &&
272
!BeEF::Filters.has_non_printable_char?(id) &&
273
id.length == 8
274
end
275
276
# Helper method that formats the given resource class in a human-readable format.
277
#
278
# @param resource [Resolv::DNS::Resource::IN] resource class
279
#
280
# @return [String] resource name stripped of any module/class names
281
def format_resource(resource)
282
/::(\w+)$/.match(resource)[1]
283
end
284
end
285
end
286
end
287
end
288
289