nesser 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +28 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/nesser.rb +149 -0
- data/lib/nesser/dns_exception.rb +18 -0
- data/lib/nesser/packets/answer.rb +79 -0
- data/lib/nesser/packets/constants.rb +111 -0
- data/lib/nesser/packets/packer.rb +97 -0
- data/lib/nesser/packets/packet.rb +186 -0
- data/lib/nesser/packets/question.rb +43 -0
- data/lib/nesser/packets/rr_types.rb +290 -0
- data/lib/nesser/packets/unpacker.rb +127 -0
- data/lib/nesser/transaction.rb +94 -0
- data/lib/nesser/version.rb +3 -0
- data/nesser.gemspec +22 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bffba7aa303feb0c9264e87bea0b4071dd43de0a
|
4
|
+
data.tar.gz: f9d20c7da26f034657c8fbb16ded9222571d1b1b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5a92a2bd9131b145d848cfc2204dbcbc5468424e43fb60cf8a8cd08d1e5480cfae325f5ec47c0e5824c20c7fcb99285a923bfa6a747e6c2bbdfaf616bb631232
|
7
|
+
data.tar.gz: 42c424af1ba877d784b05f1a40cb1577067d47ff6b06b672f62bb5f7a5393ace29abbcd8a1e305d282e098fe2751862dc4a1063637884b8c7ede1717370ec79c
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Nesser
|
2
|
+
|
3
|
+
A DNS client and server class, written for Dnscat2 (and other similar projects).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'nesser'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install nesser
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/iagox86/nesser
|
28
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "nesser"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/nesser.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# Encoding: ASCII-8BIT
|
2
|
+
##
|
3
|
+
# nesser.rb
|
4
|
+
# Created Oct 7, 2015
|
5
|
+
# By Ron Bowes
|
6
|
+
#
|
7
|
+
# See: LICENSE.md
|
8
|
+
#
|
9
|
+
# I had nothing but trouble using rubydns (which became celluloid-dns, whose
|
10
|
+
# documentation is just flat out wrong), and I only really need a very small
|
11
|
+
# subset of DNS functionality, so I decided that I should just wrote my own.
|
12
|
+
#
|
13
|
+
# Note: after writing this, I noticed that Resolv::DNS exists in the Ruby
|
14
|
+
# language, I need to check if I can use that.
|
15
|
+
#
|
16
|
+
# There are two methods for using this library: as a client (to make a query)
|
17
|
+
# or as a server (to listen for queries and respond to them).
|
18
|
+
#
|
19
|
+
# To make a query, use Nesser.query:
|
20
|
+
#
|
21
|
+
# Nesser.query("google.com") do |response|
|
22
|
+
# ...
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# `response` will be of type Nesser::Packet.
|
26
|
+
#
|
27
|
+
# To listen for queries, create a new instance of Nesser, which will begin
|
28
|
+
# listening on a port, but won't actually handle queries yet:
|
29
|
+
#
|
30
|
+
# nesser = Nesser.new("0.0.0.0", 53) do |transaction|
|
31
|
+
# ...
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# `transaction` is of type Nesser::Transaction, and allows you to respond to the
|
35
|
+
# request either immediately or asynchronously.
|
36
|
+
#
|
37
|
+
# Nesser currently supports the following record types: A, NS, CNAME, SOA, MX,
|
38
|
+
# TXT, and AAAA.
|
39
|
+
##
|
40
|
+
|
41
|
+
require 'ipaddr'
|
42
|
+
require 'socket'
|
43
|
+
require 'timeout'
|
44
|
+
|
45
|
+
require "nesser/version"
|
46
|
+
|
47
|
+
module Nesser
|
48
|
+
class Nesser
|
49
|
+
attr_reader :thread
|
50
|
+
|
51
|
+
def initialize(s:, logger:, host:"0.0.0.0", port:53)
|
52
|
+
@s = s
|
53
|
+
@s.bind(host, port)
|
54
|
+
@logger = logger
|
55
|
+
|
56
|
+
@thread = Thread.new() do
|
57
|
+
begin
|
58
|
+
loop do
|
59
|
+
# Grab all the data we can off the socket
|
60
|
+
data = @s.recvfrom(65536)
|
61
|
+
|
62
|
+
begin
|
63
|
+
# Data is an array where the first element is the actual data, and the second is the host/port
|
64
|
+
request = Packet.parse(data[0])
|
65
|
+
rescue DnsException => e
|
66
|
+
logger.error("Failed to parse the DNS packet: %s" % e.to_s())
|
67
|
+
next
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create a transaction object, which we can use to respond
|
71
|
+
transaction = Transaction.new(
|
72
|
+
s: @s,
|
73
|
+
request_packet: request,
|
74
|
+
host: data[1][3],
|
75
|
+
port: data[1][1],
|
76
|
+
)
|
77
|
+
|
78
|
+
begin
|
79
|
+
proc.call(transaction)
|
80
|
+
rescue StandardError => e
|
81
|
+
logger.error("Error thrown while processing the DNS packet: %s" % e.to_s())
|
82
|
+
logger.info(e.backtrace())
|
83
|
+
transaction.error!(RCODE_SERVER_FAILURE)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
ensure
|
87
|
+
@s.close()
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Kill the listener
|
93
|
+
def stop()
|
94
|
+
if(@thread.nil?)
|
95
|
+
@logger.error("Tried to stop a listener that wasn't listening!")
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
@thread.kill()
|
100
|
+
@thread = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# After calling on_request(), this can be called to halt the program's
|
104
|
+
# execution until the thread is stopped.
|
105
|
+
def wait()
|
106
|
+
if(@thread.nil?)
|
107
|
+
@logger.error("Tried to wait on a Nesser instance that wasn't listening!")
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
@thread.join()
|
112
|
+
end
|
113
|
+
|
114
|
+
# Send out a query, asynchronously. This immediately returns, then, when the
|
115
|
+
# query is finished, the callback block is called with a Nesser::Packet that
|
116
|
+
# represents the response (or nil, if there was a timeout).
|
117
|
+
def Nesser.query(s, hostname, params = {})
|
118
|
+
server = params[:server] || "8.8.8.8"
|
119
|
+
port = params[:port] || 53
|
120
|
+
type = params[:type] || Nesser::Packet::TYPE_A
|
121
|
+
cls = params[:cls] || Nesser::Packet::CLS_IN
|
122
|
+
timeout = params[:timeout] || 3
|
123
|
+
|
124
|
+
packet = Nesser::Packet.new(rand(65535), Nesser::Packet::QR_QUERY, Nesser::Packet::OPCODE_QUERY, Nesser::Packet::FLAG_RD, Nesser::Packet::RCODE_SUCCESS)
|
125
|
+
packet.add_question(Nesser::Packet::Question.new(hostname, type, cls))
|
126
|
+
|
127
|
+
s = UDPSocket.new()
|
128
|
+
|
129
|
+
return Thread.new() do
|
130
|
+
begin
|
131
|
+
s.send(packet.serialize(), 0, server, port)
|
132
|
+
|
133
|
+
timeout(timeout) do
|
134
|
+
response = s.recv(65536)
|
135
|
+
proc.call(Nesser::Packet.unpack(response))
|
136
|
+
end
|
137
|
+
rescue Timeout::Error
|
138
|
+
proc.call(nil)
|
139
|
+
rescue Exception => e
|
140
|
+
@logger.error("There was an exception sending a query for #{hostname} to #{server}:#{port}: #{e}")
|
141
|
+
ensure
|
142
|
+
if(s)
|
143
|
+
s.close()
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Encoding: ASCII-8BIT
|
2
|
+
##
|
3
|
+
# dns_exception.rb
|
4
|
+
# Created June 20, 2017
|
5
|
+
# By Ron Bowes
|
6
|
+
#
|
7
|
+
# See LICENSE.md
|
8
|
+
#
|
9
|
+
# Implements a simple exception class for dns errors.
|
10
|
+
##
|
11
|
+
|
12
|
+
module Nesser
|
13
|
+
class DnsException < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
class FormatException < DnsException
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Encoding: ASCII-8BIT
|
2
|
+
##
|
3
|
+
# answer.rb
|
4
|
+
# Created June 20, 2017
|
5
|
+
# By Ron Bowes
|
6
|
+
#
|
7
|
+
# See: LICENSE.md
|
8
|
+
#
|
9
|
+
# A DNS answer. A DNS response packet contains zero or more Answer records
|
10
|
+
# (defined by the 'ancount' value in the header). An answer contains the
|
11
|
+
# name of the domain from the question, followed by a resource record.
|
12
|
+
##
|
13
|
+
|
14
|
+
module Nesser
|
15
|
+
class Answer
|
16
|
+
attr_reader :name, :type, :cls, :ttl, :rr
|
17
|
+
|
18
|
+
def initialize(name:, type:, cls:, ttl:, rr:)
|
19
|
+
@name = name
|
20
|
+
@type = type
|
21
|
+
@cls = cls
|
22
|
+
@ttl = ttl
|
23
|
+
@rr = rr
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.unpack(unpacker)
|
27
|
+
name = unpacker.unpack_name()
|
28
|
+
type, cls, ttl = unpacker.unpack("nnN")
|
29
|
+
|
30
|
+
case type
|
31
|
+
when TYPE_A
|
32
|
+
rr = A.unpack(unpacker)
|
33
|
+
when TYPE_NS
|
34
|
+
rr = NS.unpack(unpacker)
|
35
|
+
when TYPE_CNAME
|
36
|
+
rr = CNAME.unpack(unpacker)
|
37
|
+
when TYPE_SOA
|
38
|
+
rr = SOA.unpack(unpacker)
|
39
|
+
when TYPE_MX
|
40
|
+
rr = MX.unpack(unpacker)
|
41
|
+
when TYPE_TXT
|
42
|
+
rr = TXT.unpack(unpacker)
|
43
|
+
when TYPE_AAAA
|
44
|
+
rr = AAAA.unpack(unpacker)
|
45
|
+
else
|
46
|
+
rr = RRUnknown.unpack(unpacker, type)
|
47
|
+
end
|
48
|
+
|
49
|
+
return self.new(
|
50
|
+
name: name,
|
51
|
+
type: type,
|
52
|
+
cls: cls,
|
53
|
+
ttl: ttl,
|
54
|
+
rr: rr,
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def pack(packer)
|
59
|
+
# The name is echoed back
|
60
|
+
packer.pack_name(@name)
|
61
|
+
|
62
|
+
# The type/class/ttl are added
|
63
|
+
packer.pack('nnN', @type, @cls, @ttl)
|
64
|
+
|
65
|
+
# Finally, the length and rr (the length is included)
|
66
|
+
@rr.pack(packer)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s()
|
70
|
+
return '%s %d [%s %s] %s' % [
|
71
|
+
@name,
|
72
|
+
@ttl,
|
73
|
+
TYPES[@type] || '<0x%04x?>' % @type,
|
74
|
+
CLSES[@cls] || '<0x%04x?>' % @cls,
|
75
|
+
@rr.to_s(),
|
76
|
+
]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# Encoding: ASCII-8BIT
|
2
|
+
##
|
3
|
+
# constants.rb
|
4
|
+
# Created June 20, 2017
|
5
|
+
# By Ron Bowes
|
6
|
+
#
|
7
|
+
# See: LICENSE.md
|
8
|
+
##
|
9
|
+
|
10
|
+
module Nesser
|
11
|
+
# Max recursion depth for parsing names
|
12
|
+
MAX_RECURSION_DEPTH = 16
|
13
|
+
|
14
|
+
# These restrictions are from RFC 952
|
15
|
+
LEGAL_CHARACTERS = (
|
16
|
+
('a'..'z').to_a +
|
17
|
+
('A'..'Z').to_a +
|
18
|
+
(0..9).to_a +
|
19
|
+
['-', '.']
|
20
|
+
)
|
21
|
+
MAX_SEGMENT_LENGTH = 63
|
22
|
+
MAX_TOTAL_LENGTH = 253
|
23
|
+
|
24
|
+
CLS_IN = 0x0001 # Internet
|
25
|
+
CLSES = {
|
26
|
+
CLS_IN => "IN",
|
27
|
+
}
|
28
|
+
|
29
|
+
# Request / response
|
30
|
+
QR_QUERY = 0x0000
|
31
|
+
QR_RESPONSE = 0x0001
|
32
|
+
|
33
|
+
QRS = {
|
34
|
+
QR_QUERY => "QUERY",
|
35
|
+
QR_RESPONSE => "RESPONSE",
|
36
|
+
}
|
37
|
+
|
38
|
+
# Return codes
|
39
|
+
RCODE_SUCCESS = 0x0000
|
40
|
+
RCODE_FORMAT_ERROR = 0x0001
|
41
|
+
RCODE_SERVER_FAILURE = 0x0002 # :servfail
|
42
|
+
RCODE_NAME_ERROR = 0x0003 # :NXDomain
|
43
|
+
RCODE_NOT_IMPLEMENTED = 0x0004
|
44
|
+
RCODE_REFUSED = 0x0005
|
45
|
+
|
46
|
+
RCODES = {
|
47
|
+
RCODE_SUCCESS => ":NoError (RCODE_SUCCESS)",
|
48
|
+
RCODE_FORMAT_ERROR => ":FormErr (RCODE_FORMAT_ERROR)",
|
49
|
+
RCODE_SERVER_FAILURE => ":ServFail (RCODE_SERVER_FAILURE)",
|
50
|
+
RCODE_NAME_ERROR => ":NXDomain (RCODE_NAME_ERROR)",
|
51
|
+
RCODE_NOT_IMPLEMENTED => ":NotImp (RCODE_NOT_IMPLEMENTED)",
|
52
|
+
RCODE_REFUSED => ":Refused (RCODE_REFUSED)",
|
53
|
+
}
|
54
|
+
|
55
|
+
# Opcodes - only QUERY is typically used
|
56
|
+
OPCODE_QUERY = 0x0000
|
57
|
+
OPCODE_IQUERY = 0x0800
|
58
|
+
OPCODE_STATUS = 0x1000
|
59
|
+
|
60
|
+
OPCODES = {
|
61
|
+
OPCODE_QUERY => "OPCODE_QUERY",
|
62
|
+
OPCODE_IQUERY => "OPCODE_IQUERY",
|
63
|
+
OPCODE_STATUS => "OPCODE_STATUS",
|
64
|
+
}
|
65
|
+
|
66
|
+
# The types that we support
|
67
|
+
TYPE_A = 0x0001
|
68
|
+
TYPE_NS = 0x0002
|
69
|
+
TYPE_CNAME = 0x0005
|
70
|
+
TYPE_SOA = 0x0006
|
71
|
+
TYPE_MX = 0x000f
|
72
|
+
TYPE_TXT = 0x0010
|
73
|
+
TYPE_AAAA = 0x001c
|
74
|
+
TYPE_ANY = 0x00FF
|
75
|
+
|
76
|
+
TYPES = {
|
77
|
+
TYPE_A => "A",
|
78
|
+
TYPE_NS => "NS",
|
79
|
+
TYPE_CNAME => "CNAME",
|
80
|
+
TYPE_SOA => "SOA",
|
81
|
+
TYPE_MX => "MX",
|
82
|
+
TYPE_TXT => "TXT",
|
83
|
+
TYPE_AAAA => "AAAA",
|
84
|
+
TYPE_ANY => "ANY",
|
85
|
+
}
|
86
|
+
|
87
|
+
# The DNS flags
|
88
|
+
FLAG_AA = 0x0008 # Authoritative answer
|
89
|
+
FLAG_TC = 0x0004 # Truncated
|
90
|
+
FLAG_RD = 0x0002 # Recursion desired
|
91
|
+
FLAG_RA = 0x0001 # Recursion available
|
92
|
+
|
93
|
+
# This converts a set of flags, as an integer, into a string
|
94
|
+
def self.FLAGS(flags)
|
95
|
+
result = []
|
96
|
+
if((flags & FLAG_AA) == FLAG_AA)
|
97
|
+
result << "AA"
|
98
|
+
end
|
99
|
+
if((flags & FLAG_TC) == FLAG_TC)
|
100
|
+
result << "TC"
|
101
|
+
end
|
102
|
+
if((flags & FLAG_RD) == FLAG_RD)
|
103
|
+
result << "RD"
|
104
|
+
end
|
105
|
+
if((flags & FLAG_RA) == FLAG_RA)
|
106
|
+
result << "RA"
|
107
|
+
end
|
108
|
+
|
109
|
+
return result.join("|")
|
110
|
+
end
|
111
|
+
end
|