nesser 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|