Skip to content

Commit e621156

Browse files
committed
Merge pull request #16 from WinRb/dan/ntlm-client
Initial go at an NTLM Client that will do session signing/sealing
2 parents 92348a0 + cfa5581 commit e621156

File tree

12 files changed

+432
-14
lines changed

12 files changed

+432
-14
lines changed

Diff for: Rakefile

+8
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ task :coverage do
1212
Rake::Task["spec"].execute
1313
end
1414

15+
desc "Open a Pry console for this library"
16+
task :console do
17+
require 'pry'
18+
require 'net/ntlm'
19+
ARGV.clear
20+
Pry.start
21+
end
22+

Diff for: lib/net/ntlm.rb

+2-4
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,9 @@
5757
require 'net/ntlm/message/type2'
5858
require 'net/ntlm/message/type3'
5959

60-
6160
require 'net/ntlm/encode_util'
6261

63-
64-
62+
require 'net/ntlm/client'
6563

6664
module Net
6765
module NTLM
@@ -135,7 +133,7 @@ def ntlm_hash(password, opt = {})
135133
# @option opt :unicode (false) Unicode encode the domain
136134
def ntlmv2_hash(user, password, target, opt={})
137135
ntlmhash = ntlm_hash(password, opt)
138-
userdomain = (user + target).upcase
136+
userdomain = user.upcase + target
139137
unless opt[:unicode]
140138
userdomain = EncodeUtil.encode_utf16le(userdomain)
141139
end

Diff for: lib/net/ntlm/client.rb

+61
Original file line numberDiff line numberDiff line change
@@ -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"

Diff for: lib/net/ntlm/client/session.rb

+223
Original file line numberDiff line numberDiff line change
@@ -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

Diff for: lib/net/ntlm/message.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ module NTLM
2020
:LOCAL_CALL => 0x00004000,
2121
:ALWAYS_SIGN => 0x00008000,
2222
:TARGET_TYPE_DOMAIN => 0x00010000,
23-
:TARGET_INFO => 0x00800000,
2423
:NTLM2_KEY => 0x00080000,
24+
:TARGET_INFO => 0x00800000,
2525
:KEY128 => 0x20000000,
2626
:KEY_EXCHANGE => 0x40000000,
2727
:KEY56 => 0x80000000

Diff for: lib/net/ntlm/message/type1.rb

-2
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,3 @@ class Type1 < Message
1616
end
1717
end
1818
end
19-
20-

Diff for: lib/net/ntlm/message/type3.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def create(arg, opt ={})
3939

4040
if arg[:session_key]
4141
t.enable(:session_key)
42-
t.session_key = arg[session_key]
42+
t.session_key = arg[:session_key]
4343
end
4444

4545
if arg[:flag]

Diff for: rubyntlm.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
2121

2222
s.license = 'MIT'
2323

24+
s.add_development_dependency "pry"
2425
s.add_development_dependency "rake"
2526
s.add_development_dependency "rspec", ">= 2.11"
2627
s.add_development_dependency "simplecov"

0 commit comments

Comments
 (0)