rubyntlm 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -3
- data/README.md +9 -2
- data/Rakefile +8 -0
- data/examples/http.rb +1 -1
- data/lib/net/ntlm.rb +10 -9
- data/lib/net/ntlm/blob.rb +17 -6
- data/lib/net/ntlm/client.rb +61 -0
- data/lib/net/ntlm/client/session.rb +223 -0
- data/lib/net/ntlm/encode_util.rb +2 -2
- data/lib/net/ntlm/field_set.rb +9 -5
- data/lib/net/ntlm/message.rb +23 -9
- data/lib/net/ntlm/message/type1.rb +1 -26
- data/lib/net/ntlm/message/type2.rb +11 -37
- data/lib/net/ntlm/message/type3.rb +77 -14
- data/lib/net/ntlm/version.rb +1 -1
- data/rubyntlm.gemspec +3 -2
- data/spec/lib/net/ntlm/blob_spec.rb +1 -1
- data/spec/lib/net/ntlm/client/session_spec.rb +68 -0
- data/spec/lib/net/ntlm/client_spec.rb +64 -0
- data/spec/lib/net/ntlm/encode_util_spec.rb +3 -3
- data/spec/lib/net/ntlm/field_set_spec.rb +4 -4
- data/spec/lib/net/ntlm/field_spec.rb +5 -5
- data/spec/lib/net/ntlm/message/type1_spec.rb +99 -10
- data/spec/lib/net/ntlm/message/type2_spec.rb +68 -24
- data/spec/lib/net/ntlm/message/type3_spec.rb +207 -2
- data/spec/lib/net/ntlm/security_buffer_spec.rb +8 -8
- data/spec/lib/net/ntlm/string_spec.rb +14 -14
- data/spec/lib/net/ntlm/version_spec.rb +7 -6
- data/spec/lib/net/ntlm_spec.rb +20 -22
- data/spec/spec_helper.rb +1 -2
- data/spec/support/shared/examples/net/ntlm/field_shared.rb +3 -3
- data/spec/support/shared/examples/net/ntlm/fieldset_shared.rb +34 -34
- data/spec/support/shared/examples/net/ntlm/int_shared.rb +8 -8
- data/spec/support/shared/examples/net/ntlm/message_shared.rb +3 -3
- metadata +36 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33cb0ea71a4b1d8149a2a8bf39c5f24bd2752d5b
|
4
|
+
data.tar.gz: 032e16ead1a05ddcf0268d79c4ffff2ff631bfcf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c276677b10f3f2b7fa35817fce82e1cfd5012298c752fbfc4576f0b4d45d339a5e6589fb547c16aa6609cb4174f2e9ac396c0f49ad7e7267f5b68b20f6e1fa47
|
7
|
+
data.tar.gz: 40e3423ad351782e571611b32704b583c0b75a06a2b3acedc62dc9c6e916b0bc3356fe51d1060dd3cda0ef330b175ad108623625b3f7a6f6eb7728f66d03216e
|
data/.rspec
CHANGED
@@ -1,3 +1,2 @@
|
|
1
|
-
--format
|
2
|
-
--
|
3
|
-
--drb
|
1
|
+
--format documentation
|
2
|
+
--color
|
data/README.md
CHANGED
@@ -6,19 +6,26 @@ Ruby/NTLM provides message creator and parser for the NTLM authentication.
|
|
6
6
|
|
7
7
|
__100% Ruby__
|
8
8
|
|
9
|
+
How to install
|
10
|
+
--------------
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
require 'rubyntlm'
|
14
|
+
```
|
15
|
+
|
9
16
|
Simple Example
|
10
17
|
--------------
|
11
18
|
|
12
19
|
### Creating NTLM Type 1 message
|
13
20
|
|
14
21
|
```ruby
|
15
|
-
t1 = NTLM::Message::Type1.new()
|
22
|
+
t1 = Net::NTLM::Message::Type1.new()
|
16
23
|
```
|
17
24
|
|
18
25
|
### Parsing NTLM Type 2 message from server
|
19
26
|
|
20
27
|
```ruby
|
21
|
-
t2 = NTLM::Message.parse(message_from_server)
|
28
|
+
t2 = Net::NTLM::Message.parse(message_from_server)
|
22
29
|
```
|
23
30
|
|
24
31
|
### Creating NTLM Type 3 message
|
data/Rakefile
CHANGED
data/examples/http.rb
CHANGED
@@ -48,7 +48,7 @@ def main
|
|
48
48
|
|
49
49
|
unless $user and $passwd
|
50
50
|
target = t2.target_name
|
51
|
-
target = Net::NTLM::decode_utf16le(target) if t2.has_flag?(:UNICODE)
|
51
|
+
target = Net::NTLM::EncodeUtil.decode_utf16le(target) if t2.has_flag?(:UNICODE)
|
52
52
|
puts "Target: #{target}"
|
53
53
|
print "User name: "
|
54
54
|
($user = $stdin.readline).chomp!
|
data/lib/net/ntlm.rb
CHANGED
@@ -57,11 +57,9 @@ require 'net/ntlm/message/type1'
|
|
57
57
|
require 'net/ntlm/message/type2'
|
58
58
|
require 'net/ntlm/message/type3'
|
59
59
|
|
60
|
-
|
61
60
|
require 'net/ntlm/encode_util'
|
62
61
|
|
63
|
-
|
64
|
-
|
62
|
+
require 'net/ntlm/client'
|
65
63
|
|
66
64
|
module Net
|
67
65
|
module NTLM
|
@@ -102,10 +100,11 @@ module Net
|
|
102
100
|
end
|
103
101
|
|
104
102
|
def apply_des(plain, keys)
|
105
|
-
dec = OpenSSL::Cipher::
|
103
|
+
dec = OpenSSL::Cipher::Cipher.new("des-cbc")
|
104
|
+
dec.padding = 0
|
106
105
|
keys.map {|k|
|
107
106
|
dec.key = k
|
108
|
-
dec.encrypt.update(plain)
|
107
|
+
dec.encrypt.update(plain) + dec.final
|
109
108
|
}
|
110
109
|
end
|
111
110
|
|
@@ -134,7 +133,7 @@ module Net
|
|
134
133
|
# @option opt :unicode (false) Unicode encode the domain
|
135
134
|
def ntlmv2_hash(user, password, target, opt={})
|
136
135
|
ntlmhash = ntlm_hash(password, opt)
|
137
|
-
userdomain =
|
136
|
+
userdomain = user.upcase + target
|
138
137
|
unless opt[:unicode]
|
139
138
|
userdomain = EncodeUtil.encode_utf16le(userdomain)
|
140
139
|
end
|
@@ -184,7 +183,7 @@ module Net
|
|
184
183
|
ts = Time.now.to_i
|
185
184
|
end
|
186
185
|
# epoch -> milsec from Jan 1, 1601
|
187
|
-
ts =
|
186
|
+
ts = 10_000_000 * (ts + TIME_OFFSET)
|
188
187
|
|
189
188
|
blob = Blob.new
|
190
189
|
blob.timestamp = ts
|
@@ -192,6 +191,7 @@ module Net
|
|
192
191
|
blob.target_info = ti
|
193
192
|
|
194
193
|
bb = blob.serialize
|
194
|
+
|
195
195
|
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb
|
196
196
|
end
|
197
197
|
|
@@ -218,15 +218,16 @@ module Net
|
|
218
218
|
rescue
|
219
219
|
raise ArgumentError
|
220
220
|
end
|
221
|
+
chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer)
|
221
222
|
|
222
223
|
if opt[:client_challenge]
|
223
|
-
cc
|
224
|
+
cc = opt[:client_challenge]
|
224
225
|
else
|
225
226
|
cc = rand(MAX64)
|
226
227
|
end
|
227
228
|
cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
|
228
229
|
|
229
|
-
keys = gen_keys
|
230
|
+
keys = gen_keys(passwd_hash.ljust(21, "\0"))
|
230
231
|
session_hash = OpenSSL::Digest::MD5.digest(chal + cc).slice(0, 8)
|
231
232
|
response = apply_des(session_hash, keys).join
|
232
233
|
[cc.ljust(24, "\0"), response]
|
data/lib/net/ntlm/blob.rb
CHANGED
@@ -4,14 +4,25 @@ module Net
|
|
4
4
|
BLOB_SIGN = 0x00000101
|
5
5
|
|
6
6
|
class Blob < FieldSet
|
7
|
-
int32LE :blob_signature,
|
8
|
-
int32LE :reserved,
|
7
|
+
int32LE :blob_signature, {:value => BLOB_SIGN}
|
8
|
+
int32LE :reserved, {:value => 0}
|
9
9
|
int64LE :timestamp, {:value => 0}
|
10
10
|
string :challenge, {:value => "", :size => 8}
|
11
|
-
int32LE :unknown1,
|
12
|
-
string :target_info,
|
13
|
-
int32LE :unknown2,
|
11
|
+
int32LE :unknown1, {:value => 0}
|
12
|
+
string :target_info, {:value => "", :size => 0}
|
13
|
+
int32LE :unknown2, {:value => 0}
|
14
|
+
|
15
|
+
def parse(str, offset=0)
|
16
|
+
# 28 is the length of all fields before the variable-length
|
17
|
+
# target_info field.
|
18
|
+
if str.size > 28
|
19
|
+
enable(:target_info)
|
20
|
+
# Grab everything except the last 4 bytes (which will be :unknown2)
|
21
|
+
self[:target_info].value = str[28..-5]
|
22
|
+
end
|
23
|
+
super
|
24
|
+
end
|
14
25
|
end
|
15
26
|
|
16
27
|
end
|
17
|
-
end
|
28
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Net
|
2
|
+
module NTLM
|
3
|
+
class Client
|
4
|
+
|
5
|
+
DEFAULT_FLAGS = NTLM::FLAGS[:UNICODE] | NTLM::FLAGS[:OEM] |
|
6
|
+
NTLM::FLAGS[:SIGN] | NTLM::FLAGS[:SEAL] | NTLM::FLAGS[:REQUEST_TARGET] |
|
7
|
+
NTLM::FLAGS[:NTLM] | NTLM::FLAGS[:ALWAYS_SIGN] | NTLM::FLAGS[:NTLM2_KEY] |
|
8
|
+
NTLM::FLAGS[:KEY128] | NTLM::FLAGS[:KEY_EXCHANGE] | NTLM::FLAGS[:KEY56]
|
9
|
+
|
10
|
+
attr_reader :username, :password, :domain, :workstation, :flags
|
11
|
+
|
12
|
+
# @note All string parameters should be encoded in UTF-8. The proper
|
13
|
+
# final encoding for placing in the various {Message messages} will be
|
14
|
+
# chosen based on negotiation with the server.
|
15
|
+
#
|
16
|
+
# @param username [String]
|
17
|
+
# @param password [String]
|
18
|
+
# @option opts [String] :domain where we're authenticating to
|
19
|
+
# @option opts [String] :workstation local workstation name
|
20
|
+
# @option opts [Fixnum] :flags (DEFAULT_FLAGS) see Net::NTLM::Message::Type1.flag
|
21
|
+
def initialize(username, password, opts = {})
|
22
|
+
@username = username
|
23
|
+
@password = password
|
24
|
+
@domain = opts[:domain] || nil
|
25
|
+
@workstation = opts[:workstation] || nil
|
26
|
+
@flags = opts[:flags] || DEFAULT_FLAGS
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [NTLM::Message]
|
30
|
+
def init_context(resp = nil)
|
31
|
+
if resp.nil?
|
32
|
+
@session = nil
|
33
|
+
type1_message
|
34
|
+
else
|
35
|
+
@session = Client::Session.new(self, Net::NTLM::Message.decode64(resp))
|
36
|
+
@session.authenticate!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Client::Session]
|
41
|
+
def session
|
42
|
+
@session
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @return [Message::Type1]
|
48
|
+
def type1_message
|
49
|
+
type1 = Message::Type1.new
|
50
|
+
type1[:flag].value = flags
|
51
|
+
type1.domain = domain if domain
|
52
|
+
type1.workstation = workstation if workstation
|
53
|
+
|
54
|
+
type1
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
require "net/ntlm/client/session"
|
@@ -0,0 +1,223 @@
|
|
1
|
+
module Net
|
2
|
+
module NTLM
|
3
|
+
class Client::Session
|
4
|
+
|
5
|
+
VERSION_MAGIC = "\x01\x00\x00\x00"
|
6
|
+
TIME_OFFSET = 11644473600
|
7
|
+
MAX64 = 0xffffffffffffffff
|
8
|
+
CLIENT_TO_SERVER_SIGNING = "session key to client-to-server signing key magic constant\0"
|
9
|
+
SERVER_TO_CLIENT_SIGNING = "session key to server-to-client signing key magic constant\0"
|
10
|
+
CLIENT_TO_SERVER_SEALING = "session key to client-to-server sealing key magic constant\0"
|
11
|
+
SERVER_TO_CLIENT_SEALING = "session key to server-to-client sealing key magic constant\0"
|
12
|
+
|
13
|
+
attr_reader :client, :challenge_message
|
14
|
+
|
15
|
+
# @param client [Net::NTLM::Client] the client instance
|
16
|
+
# @param challenge_message [Net::NTLM::Message::Type2] server message
|
17
|
+
def initialize(client, challenge_message)
|
18
|
+
@client = client
|
19
|
+
@challenge_message = challenge_message
|
20
|
+
end
|
21
|
+
|
22
|
+
# Generate an NTLMv2 AUTHENTICATE_MESSAGE
|
23
|
+
# @see http://msdn.microsoft.com/en-us/library/cc236643.aspx
|
24
|
+
# @return [Net::NTLM::Message::Type3]
|
25
|
+
def authenticate!
|
26
|
+
calculate_user_session_key!
|
27
|
+
type3_opts = {
|
28
|
+
:lm_response => lmv2_resp,
|
29
|
+
:ntlm_response => ntlmv2_resp,
|
30
|
+
:domain => domain,
|
31
|
+
:user => username,
|
32
|
+
:workstation => workstation,
|
33
|
+
:flag => (challenge_message.flag & client.flags)
|
34
|
+
}
|
35
|
+
t3 = Message::Type3.create type3_opts
|
36
|
+
if negotiate_key_exchange?
|
37
|
+
t3.enable(:session_key)
|
38
|
+
rc4 = OpenSSL::Cipher::Cipher.new("rc4")
|
39
|
+
rc4.encrypt
|
40
|
+
rc4.key = user_session_key
|
41
|
+
sk = rc4.update master_key
|
42
|
+
sk << rc4.final
|
43
|
+
t3.session_key = sk
|
44
|
+
end
|
45
|
+
t3
|
46
|
+
end
|
47
|
+
|
48
|
+
def sign_message(message)
|
49
|
+
seq = sequence
|
50
|
+
sig = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, client_sign_key, "#{seq}#{message}")[0..7]
|
51
|
+
if negotiate_key_exchange?
|
52
|
+
sig = client_cipher.update sig
|
53
|
+
sig << client_cipher.final
|
54
|
+
end
|
55
|
+
"#{VERSION_MAGIC}#{sig}#{seq}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def verify_signature(signature, message)
|
59
|
+
seq = signature[-4..-1]
|
60
|
+
sig = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, server_sign_key, "#{seq}#{message}")[0..7]
|
61
|
+
if negotiate_key_exchange?
|
62
|
+
sig = server_cipher.update sig
|
63
|
+
sig << server_cipher.final
|
64
|
+
end
|
65
|
+
"#{VERSION_MAGIC}#{sig}#{seq}" == signature
|
66
|
+
end
|
67
|
+
|
68
|
+
def seal_message(message)
|
69
|
+
emessage = client_cipher.update(message)
|
70
|
+
emessage + client_cipher.final
|
71
|
+
end
|
72
|
+
|
73
|
+
def unseal_message(emessage)
|
74
|
+
message = server_cipher.update(emessage)
|
75
|
+
message + server_cipher.final
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
|
82
|
+
def user_session_key
|
83
|
+
@user_session_key ||= nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def master_key
|
87
|
+
@master_key ||= begin
|
88
|
+
if negotiate_key_exchange?
|
89
|
+
OpenSSL::Cipher.new("rc4").random_key
|
90
|
+
else
|
91
|
+
user_session_key
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def sequence
|
97
|
+
[raw_sequence].pack("V*")
|
98
|
+
end
|
99
|
+
|
100
|
+
def raw_sequence
|
101
|
+
if defined? @raw_sequence
|
102
|
+
@raw_sequence += 1
|
103
|
+
else
|
104
|
+
@raw_sequence = 0
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def client_sign_key
|
109
|
+
@client_sign_key ||= OpenSSL::Digest::MD5.digest "#{master_key}#{CLIENT_TO_SERVER_SIGNING}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def server_sign_key
|
113
|
+
@server_sign_key ||= OpenSSL::Digest::MD5.digest "#{master_key}#{SERVER_TO_CLIENT_SIGNING}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def client_seal_key
|
117
|
+
@client_seal_key ||= OpenSSL::Digest::MD5.digest "#{master_key}#{CLIENT_TO_SERVER_SEALING}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def server_seal_key
|
121
|
+
@server_seal_key ||= OpenSSL::Digest::MD5.digest "#{master_key}#{SERVER_TO_CLIENT_SEALING}"
|
122
|
+
end
|
123
|
+
|
124
|
+
def client_cipher
|
125
|
+
@client_cipher ||=
|
126
|
+
begin
|
127
|
+
rc4 = OpenSSL::Cipher::Cipher.new("rc4")
|
128
|
+
rc4.encrypt
|
129
|
+
rc4.key = client_seal_key
|
130
|
+
rc4
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def server_cipher
|
135
|
+
@server_cipher ||=
|
136
|
+
begin
|
137
|
+
rc4 = OpenSSL::Cipher::Cipher.new("rc4")
|
138
|
+
rc4.decrypt
|
139
|
+
rc4.key = server_seal_key
|
140
|
+
rc4
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def client_challenge
|
145
|
+
@client_challenge ||= NTLM.pack_int64le(rand(MAX64))
|
146
|
+
end
|
147
|
+
|
148
|
+
def server_challenge
|
149
|
+
@server_challenge ||= challenge_message[:challenge].serialize
|
150
|
+
end
|
151
|
+
|
152
|
+
# epoch -> milsec from Jan 1, 1601
|
153
|
+
# @see http://support.microsoft.com/kb/188768
|
154
|
+
def timestamp
|
155
|
+
@timestamp ||= 10_000_000 * (Time.now.to_i + TIME_OFFSET)
|
156
|
+
end
|
157
|
+
|
158
|
+
def use_oem_strings?
|
159
|
+
challenge_message.has_flag? :OEM
|
160
|
+
end
|
161
|
+
|
162
|
+
def negotiate_key_exchange?
|
163
|
+
challenge_message.has_flag? :KEY_EXCHANGE
|
164
|
+
end
|
165
|
+
|
166
|
+
def username
|
167
|
+
oem_or_unicode_str client.username
|
168
|
+
end
|
169
|
+
|
170
|
+
def password
|
171
|
+
oem_or_unicode_str client.password
|
172
|
+
end
|
173
|
+
|
174
|
+
def workstation
|
175
|
+
(client.domain ? oem_or_unicode_str(client.workstation) : "")
|
176
|
+
end
|
177
|
+
|
178
|
+
def domain
|
179
|
+
(client.domain ? oem_or_unicode_str(client.domain) : "")
|
180
|
+
end
|
181
|
+
|
182
|
+
def oem_or_unicode_str(str)
|
183
|
+
if use_oem_strings?
|
184
|
+
NTLM::EncodeUtil.decode_utf16le str
|
185
|
+
else
|
186
|
+
NTLM::EncodeUtil.encode_utf16le str
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def ntlmv2_hash
|
191
|
+
@ntlmv2_hash ||= NTLM.ntlmv2_hash(username, password, domain, {:client_challenge => client_challenge, :unicode => !use_oem_strings?})
|
192
|
+
end
|
193
|
+
|
194
|
+
def calculate_user_session_key!
|
195
|
+
@user_session_key = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_hash, nt_proof_str)
|
196
|
+
end
|
197
|
+
|
198
|
+
def lmv2_resp
|
199
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_hash, server_challenge + client_challenge) + client_challenge
|
200
|
+
end
|
201
|
+
|
202
|
+
def ntlmv2_resp
|
203
|
+
nt_proof_str + blob
|
204
|
+
end
|
205
|
+
|
206
|
+
def nt_proof_str
|
207
|
+
@nt_proof_str ||= OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_hash, server_challenge + blob)
|
208
|
+
end
|
209
|
+
|
210
|
+
def blob
|
211
|
+
@blob ||=
|
212
|
+
begin
|
213
|
+
b = Blob.new
|
214
|
+
b.timestamp = timestamp
|
215
|
+
b.challenge = client_challenge
|
216
|
+
b.target_info = challenge_message.target_info
|
217
|
+
b.serialize
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|