pyu-ntlm-http 0.1.1.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.
- data/README +25 -0
- data/Rakefile +66 -0
- data/examples/http.rb +86 -0
- data/examples/imap.rb +73 -0
- data/examples/smtp.rb +94 -0
- data/lib/net/ntlm.rb +774 -0
- data/lib/net/ntlm_http.rb +853 -0
- data/ntlm-http.gemspec +16 -0
- data/test/function_test.rb +111 -0
- metadata +74 -0
data/README
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
= Ruby/NTLM -- NTLM Authentication Library for Ruby
|
2
|
+
|
3
|
+
Ruby/NTLM provides message creator and parser for the NTLM authentication.
|
4
|
+
|
5
|
+
Some features:
|
6
|
+
* Independent from non-standard Ruby libraries.
|
7
|
+
* Supports NTLM and NTLMv2 reponses.
|
8
|
+
|
9
|
+
== Simple Example
|
10
|
+
|
11
|
+
* Creating NTLM Type 1 message
|
12
|
+
|
13
|
+
t1 = NTLM::Message::Type1.new()
|
14
|
+
|
15
|
+
* Parsing NTLM Type 2 message from server
|
16
|
+
|
17
|
+
t2 = NTLM::Message.parse(message_from_server)
|
18
|
+
|
19
|
+
* Creating NTLM Type 3 message
|
20
|
+
|
21
|
+
t3 = t2.response({:user => 'user', :password => 'passwd'})
|
22
|
+
|
23
|
+
== Support
|
24
|
+
|
25
|
+
You can find Ruby/NTLM RubyForge page at http://rubyforge.org/projects/rubyntlm.
|
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Rakefile for rubyntlm -*- ruby -*-
|
2
|
+
# $Id: Rakefile,v 1.2 2006/10/05 01:36:52 koheik Exp $
|
3
|
+
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'rake/packagetask'
|
7
|
+
require 'rake/gempackagetask'
|
8
|
+
require File.join(File.dirname(__FILE__), 'lib', 'net', 'ntlm')
|
9
|
+
|
10
|
+
PKG_NAME = 'rubyntlm'
|
11
|
+
PKG_VERSION = Net::NTLM::VERSION::STRING
|
12
|
+
|
13
|
+
task :default => [:test]
|
14
|
+
|
15
|
+
Rake::TestTask.new(:test) do |t|
|
16
|
+
t.test_files = FileList[ "test/*.rb" ]
|
17
|
+
t.warning = true
|
18
|
+
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
# Rake::PackageTask.new(PKG_NAME, PKG_VERSION) do |p|
|
22
|
+
# p.need_tar_gz = true
|
23
|
+
# p.package_dir = 'build'
|
24
|
+
# p.package_files.include("README", "Rakefile")
|
25
|
+
# p.package_files.include("lib/net/**/*.rb", "test/**/*.rb", "examples/**/*.rb")
|
26
|
+
# end
|
27
|
+
|
28
|
+
Rake::RDocTask.new do |rd|
|
29
|
+
rd.rdoc_dir = 'doc'
|
30
|
+
rd.title = 'Ruby/NTLM library'
|
31
|
+
rd.main = "README"
|
32
|
+
rd.rdoc_files.include("README", "lib/**/*.rb")
|
33
|
+
end
|
34
|
+
|
35
|
+
dist_dirs = ["lib", "test", "examples"]
|
36
|
+
spec = Gem::Specification.new do |s|
|
37
|
+
s.name = PKG_NAME
|
38
|
+
s.version = PKG_VERSION
|
39
|
+
s.summary = %q{Ruby/NTLM library.}
|
40
|
+
s.description = %q{Ruby/NTLM provides message creator and parser for the NTLM authentication.}
|
41
|
+
s.authors = ["Kohei Kajimoto"]
|
42
|
+
s.email = %q{koheik@gmail.com}
|
43
|
+
s.homepage = %q{http://rubyforge.org/projects/rubyntlm}
|
44
|
+
s.rubyforge_project = %q{rubyntlm}
|
45
|
+
|
46
|
+
s.files = ["Rakefile", "README"]
|
47
|
+
dist_dirs.each do |dir|
|
48
|
+
s.files = s.files + Dir.glob("#{dir}/**/*.rb")
|
49
|
+
end
|
50
|
+
|
51
|
+
s.has_rdoc = true
|
52
|
+
s.extra_rdoc_files = %w( README )
|
53
|
+
s.rdoc_options.concat ['--main', 'README']
|
54
|
+
|
55
|
+
s.autorequire = 'net/ntlm'
|
56
|
+
end
|
57
|
+
|
58
|
+
Rake::GemPackageTask.new(spec) do |p|
|
59
|
+
p.gem_spec = spec
|
60
|
+
p.need_tar = true
|
61
|
+
p.need_zip = true
|
62
|
+
p.package_dir = 'build'
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
data/examples/http.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# $Id: http.rb,v 1.2 2006/10/05 01:36:52 koheik Exp $
|
2
|
+
require 'socket'
|
3
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
4
|
+
require 'net/ntlm'
|
5
|
+
|
6
|
+
$user = nil
|
7
|
+
$passwd = nil
|
8
|
+
|
9
|
+
$host = "www"
|
10
|
+
$port = 80
|
11
|
+
|
12
|
+
def header(f, host)
|
13
|
+
f.print "GET / HTTP/1.1\r\n"
|
14
|
+
f.print "Host: #{host}\r\n"
|
15
|
+
f.print "Keep-Alive: 300\r\n"
|
16
|
+
f.print "Connection: keep-alive\r\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
def main
|
20
|
+
|
21
|
+
s = TCPSocket.new($host, $port)
|
22
|
+
|
23
|
+
# client -> server
|
24
|
+
t1 = Net::NTLM::Message::Type1.new()
|
25
|
+
header(s, $host)
|
26
|
+
s.print "Authorization: NTLM " + t1.encode64 + "\r\n"
|
27
|
+
s.print "\r\n"
|
28
|
+
|
29
|
+
# server -> client
|
30
|
+
length = 0
|
31
|
+
while(line = s.gets)
|
32
|
+
|
33
|
+
if /^WWW-Authenticate: (NTLM|Negotiate) (.+)\r\n/ =~ line
|
34
|
+
msg = $2
|
35
|
+
end
|
36
|
+
|
37
|
+
if /^Content-Length: (\d+)\r\n/ =~ line
|
38
|
+
length = $1.to_i
|
39
|
+
end
|
40
|
+
if /^\r\n/ =~ line
|
41
|
+
if length > 0
|
42
|
+
cont = s.read(length)
|
43
|
+
end
|
44
|
+
break
|
45
|
+
end
|
46
|
+
end
|
47
|
+
t2 = Net::NTLM::Message.decode64(msg)
|
48
|
+
|
49
|
+
unless $user and $passwd
|
50
|
+
target = t2.target_name
|
51
|
+
target = Net::NTLM::decode_utf16le(target) if t2.has_flag?(:UNICODE)
|
52
|
+
puts "Target: #{target}"
|
53
|
+
print "User name: "
|
54
|
+
($user = $stdin.readline).chomp!
|
55
|
+
print "Password: "
|
56
|
+
($passwd = $stdin.readline).chomp!
|
57
|
+
end
|
58
|
+
|
59
|
+
# client -> server, again
|
60
|
+
t3 = t2.response({:user => $user, :password => $passwd}, {:ntlmv2 => true})
|
61
|
+
header(s, $host)
|
62
|
+
s.print "Authorization: NTLM " + t3.encode64 + "\r\n"
|
63
|
+
s.print "\r\n"
|
64
|
+
|
65
|
+
# server -> client
|
66
|
+
length = 0
|
67
|
+
while(line = s.gets)
|
68
|
+
|
69
|
+
if /^WWW-Authenticate: (NTLM|Negotiate) (.+)\r\n/ =~ line
|
70
|
+
msg = $2
|
71
|
+
end
|
72
|
+
|
73
|
+
if /^Content-Length: (\d+)\r\n/ =~ line
|
74
|
+
length = $1.to_i
|
75
|
+
end
|
76
|
+
if /^\r\n/ =~ line
|
77
|
+
if length > 0
|
78
|
+
p cont = s.read(length)
|
79
|
+
end
|
80
|
+
break
|
81
|
+
end
|
82
|
+
end
|
83
|
+
s.close
|
84
|
+
end
|
85
|
+
|
86
|
+
main
|
data/examples/imap.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# $Id: imap.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $
|
2
|
+
|
3
|
+
require "net/imap"
|
4
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
5
|
+
require "net/ntlm"
|
6
|
+
|
7
|
+
Net::IMAP::debug = true
|
8
|
+
|
9
|
+
$host = "localhost"
|
10
|
+
$port = 143
|
11
|
+
$ssl = false
|
12
|
+
$user = nil
|
13
|
+
$passwd = nil
|
14
|
+
|
15
|
+
module Net
|
16
|
+
class IMAP
|
17
|
+
class NtlmAuthenticator
|
18
|
+
def process(data)
|
19
|
+
case @state
|
20
|
+
when 1
|
21
|
+
@state = 2
|
22
|
+
t1 = Net::NTLM::Message::Type1.new()
|
23
|
+
return t1.serialize
|
24
|
+
when 2
|
25
|
+
@state = 3
|
26
|
+
t2 = Net::NTLM::Message.parse(data)
|
27
|
+
t3 = t2.response({:user => @user, :password => @password}, {:ntlmv2 => (@ntlm_type == "ntlmv2")})
|
28
|
+
return t3.serialize
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def initialize(user, password, ntlm_type = "ntlmv2")
|
35
|
+
@user = user
|
36
|
+
@password = password
|
37
|
+
@ntlm_type = @ntlm_type
|
38
|
+
@state = 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
add_authenticator "NTLM", NtlmAuthenticator
|
42
|
+
|
43
|
+
class ResponseParser
|
44
|
+
def continue_req
|
45
|
+
match(T_PLUS)
|
46
|
+
if lookahead.symbol == T_CRLF # means empty message
|
47
|
+
return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
|
48
|
+
end
|
49
|
+
match(T_SPACE)
|
50
|
+
return ContinuationRequest.new(resp_text, @str)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
unless $user and $passwd
|
57
|
+
print "User name: "
|
58
|
+
($user = $stdin.readline).chomp!
|
59
|
+
print "Password: "
|
60
|
+
($passwd = $stdin.readline).chomp!
|
61
|
+
end
|
62
|
+
|
63
|
+
imap = Net::IMAP.new($host, $port, $ssl)
|
64
|
+
imap.authenticate("NTLM", $user, $passwd)
|
65
|
+
imap.examine("Inbox")
|
66
|
+
# imap.search(["RECENT"]).each do |message_id|
|
67
|
+
# envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
|
68
|
+
# from = envelope.from.nil? ? "" : envelope.from[0].name
|
69
|
+
# subject = envelope.subject
|
70
|
+
# puts "#{message_id} #{from}: \t#{subject}"
|
71
|
+
# end
|
72
|
+
imap.logout
|
73
|
+
# imap.disconnect
|
data/examples/smtp.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# $Id: smtp.rb,v 1.2 2006/10/05 01:36:52 koheik Exp $
|
2
|
+
require 'socket'
|
3
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
4
|
+
require 'net/ntlm'
|
5
|
+
|
6
|
+
$user = nil
|
7
|
+
$passwd = nil
|
8
|
+
|
9
|
+
$host = "localhost"
|
10
|
+
$port = 25
|
11
|
+
|
12
|
+
$debug = true
|
13
|
+
|
14
|
+
def readline(f)
|
15
|
+
(l = f.gets).chomp!
|
16
|
+
puts "srv> " + l if $debug
|
17
|
+
l
|
18
|
+
end
|
19
|
+
|
20
|
+
def writeline(f, str)
|
21
|
+
puts "cli> " + str if $debug
|
22
|
+
f.print str + "\r\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
def main
|
26
|
+
s = TCPSocket.new($host, $port)
|
27
|
+
|
28
|
+
# greetings
|
29
|
+
readline s
|
30
|
+
writeline s, "EHLO #{$host}"
|
31
|
+
while(line = readline(s))
|
32
|
+
login = true if /^250-AUTH=LOGIN/ =~ line
|
33
|
+
ntlm = true if /^250-AUTH.+NTLM.*/ =~ line
|
34
|
+
break if /^250 OK/ =~ line
|
35
|
+
end
|
36
|
+
unless ntlm and login
|
37
|
+
raise RuntimeError, "it looks like the server doesn't support NTLM Login"
|
38
|
+
end
|
39
|
+
|
40
|
+
# send Type1 Message
|
41
|
+
t1 = Net::NTLM::Message::Type1.new()
|
42
|
+
writeline s, "AUTH NTLM " + t1.encode64
|
43
|
+
|
44
|
+
# receive Type2 Message, i hope
|
45
|
+
line = readline s
|
46
|
+
unless /334 (.+)/ =~ line
|
47
|
+
raise RuntimeError, "i don't recognize this: #{line}"
|
48
|
+
end
|
49
|
+
t2 = Net::NTLM::Message.decode64($1)
|
50
|
+
|
51
|
+
unless $user and $passwd
|
52
|
+
target = t2.target_name
|
53
|
+
target = Net::NTLM::decode_utf16le(target) if t2.has_flag?(:UNICODE)
|
54
|
+
puts "Target: #{target}"
|
55
|
+
print "User name: "
|
56
|
+
($user = $stdin.readline).chomp!
|
57
|
+
print "Password: "
|
58
|
+
($passwd = $stdin.readline).chomp!
|
59
|
+
end
|
60
|
+
|
61
|
+
# send Type3 Message
|
62
|
+
t3 = t2.response({:user => $user, :password => $passwd}, {:ntlmv2 => true})
|
63
|
+
writeline s, t3.encode64
|
64
|
+
|
65
|
+
# and result is...
|
66
|
+
line = readline s
|
67
|
+
|
68
|
+
unless /^235(.+)Authentication successful./i =~ line
|
69
|
+
raise RuntimeError, "sorry, authentication failed."
|
70
|
+
end
|
71
|
+
|
72
|
+
# do real job here like...
|
73
|
+
# from = $user
|
74
|
+
# to = "billg"
|
75
|
+
# writeline s, "MAIL FROM: #{from}"
|
76
|
+
# readline s
|
77
|
+
# writeline s, "RCPT TO: #{to}"
|
78
|
+
# readline s
|
79
|
+
# writeline s, "DATA"
|
80
|
+
# readline s
|
81
|
+
# writeline s, "From: #{from}"
|
82
|
+
# writeline s, "To: #{to}"
|
83
|
+
# writeline s, "blab blab blab..."
|
84
|
+
# writeline s, "#{from}"
|
85
|
+
# writeline s, "."
|
86
|
+
# readline s
|
87
|
+
|
88
|
+
# say bye
|
89
|
+
writeline s, "QUIT"
|
90
|
+
s.close
|
91
|
+
end
|
92
|
+
|
93
|
+
main
|
94
|
+
|
data/lib/net/ntlm.rb
ADDED
@@ -0,0 +1,774 @@
|
|
1
|
+
#
|
2
|
+
# = net/ntlm.rb
|
3
|
+
#
|
4
|
+
# An NTLM Authentication Library for Ruby
|
5
|
+
#
|
6
|
+
# This code is a derivative of "dbf2.rb" written by yrock
|
7
|
+
# and Minero Aoki. You can find original code here:
|
8
|
+
# http://jp.rubyist.net/magazine/?0013-CodeReview
|
9
|
+
# -------------------------------------------------------------
|
10
|
+
# Copyright (c) 2005,2006 yrock
|
11
|
+
#
|
12
|
+
# This program is free software.
|
13
|
+
# You can distribute/modify this program under the terms of the
|
14
|
+
# Ruby License.
|
15
|
+
#
|
16
|
+
# 2006-02-11 refactored by Minero Aoki
|
17
|
+
# -------------------------------------------------------------
|
18
|
+
#
|
19
|
+
# All protocol information used to write this code stems from
|
20
|
+
# "The NTLM Authentication Protocol" by Eric Glass. The author
|
21
|
+
# would thank to him for this tremendous work and making it
|
22
|
+
# available on the net.
|
23
|
+
# http://davenport.sourceforge.net/ntlm.html
|
24
|
+
# -------------------------------------------------------------
|
25
|
+
# Copyright (c) 2003 Eric Glass
|
26
|
+
#
|
27
|
+
# Permission to use, copy, modify, and distribute this document
|
28
|
+
# for any purpose and without any fee is hereby granted,
|
29
|
+
# provided that the above copyright notice and this list of
|
30
|
+
# conditions appear in all copies.
|
31
|
+
# -------------------------------------------------------------
|
32
|
+
#
|
33
|
+
# The author also looked Mozilla-Firefox-1.0.7 source code,
|
34
|
+
# namely, security/manager/ssl/src/nsNTLMAuthModule.cpp and
|
35
|
+
# Jonathan Bastien-Filiatrault's libntlm-ruby.
|
36
|
+
# "http://x2a.org/websvn/filedetails.php?
|
37
|
+
# repname=libntlm-ruby&path=%2Ftrunk%2Fntlm.rb&sc=1"
|
38
|
+
# The latter has a minor bug in its separate_keys function.
|
39
|
+
# The third key has to begin from the 14th character of the
|
40
|
+
# input string instead of 13th:)
|
41
|
+
#--
|
42
|
+
# $Id: ntlm.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $
|
43
|
+
#++
|
44
|
+
|
45
|
+
require 'base64'
|
46
|
+
require 'openssl'
|
47
|
+
require 'openssl/digest'
|
48
|
+
|
49
|
+
module Net #:nodoc:
|
50
|
+
module NTLM
|
51
|
+
|
52
|
+
module VERSION #:nodoc:
|
53
|
+
MAJOR = 0
|
54
|
+
MINOR = 1
|
55
|
+
TINY = 1
|
56
|
+
STRING = [MAJOR, MINOR, TINY].join('.')
|
57
|
+
end
|
58
|
+
|
59
|
+
SSP_SIGN = "NTLMSSP\0"
|
60
|
+
BLOB_SIGN = 0x00000101
|
61
|
+
LM_MAGIC = "KGS!@\#$%"
|
62
|
+
TIME_OFFSET = 11644473600
|
63
|
+
MAX64 = 0xffffffffffffffff
|
64
|
+
|
65
|
+
FLAGS = {
|
66
|
+
:UNICODE => 0x00000001,
|
67
|
+
:OEM => 0x00000002,
|
68
|
+
:REQUEST_TARGET => 0x00000004,
|
69
|
+
# :UNKNOWN => 0x00000008,
|
70
|
+
:SIGN => 0x00000010,
|
71
|
+
:SEAL => 0x00000020,
|
72
|
+
# :UNKNOWN => 0x00000040,
|
73
|
+
:NETWARE => 0x00000100,
|
74
|
+
:NTLM => 0x00000200,
|
75
|
+
# :UNKNOWN => 0x00000400,
|
76
|
+
# :UNKNOWN => 0x00000800,
|
77
|
+
:DOMAIN_SUPPLIED => 0x00001000,
|
78
|
+
:WORKSTATION_SUPPLIED => 0x00002000,
|
79
|
+
:LOCAL_CALL => 0x00004000,
|
80
|
+
:ALWAYS_SIGN => 0x00008000,
|
81
|
+
:TARGET_TYPE_DOMAIN => 0x00010000,
|
82
|
+
:TARGET_INFO => 0x00800000,
|
83
|
+
:NTLM2_KEY => 0x00080000,
|
84
|
+
:KEY128 => 0x20000000,
|
85
|
+
:KEY56 => 0x80000000
|
86
|
+
}
|
87
|
+
|
88
|
+
FLAG_KEYS = FLAGS.keys.sort{|a, b| FLAGS[a] <=> FLAGS[b] }
|
89
|
+
|
90
|
+
DEFAULT_FLAGS = {
|
91
|
+
:TYPE1 => FLAGS[:UNICODE] | FLAGS[:OEM] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY],
|
92
|
+
:TYPE2 => FLAGS[:UNICODE],
|
93
|
+
:TYPE3 => FLAGS[:UNICODE] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY]
|
94
|
+
}
|
95
|
+
|
96
|
+
# module functions
|
97
|
+
class << self
|
98
|
+
def decode_utf16le(str)
|
99
|
+
Kconv.kconv(swap16(str), Kconv::ASCII, Kconv::UTF16)
|
100
|
+
end
|
101
|
+
|
102
|
+
def encode_utf16le(str)
|
103
|
+
swap16(Kconv.kconv(str, Kconv::UTF16, Kconv::ASCII))
|
104
|
+
end
|
105
|
+
|
106
|
+
def pack_int64le(val)
|
107
|
+
[val & 0x00000000ffffffff, val >> 32].pack("V2")
|
108
|
+
end
|
109
|
+
|
110
|
+
def swap16(str)
|
111
|
+
str.unpack("v*").pack("n*")
|
112
|
+
end
|
113
|
+
|
114
|
+
def split7(str)
|
115
|
+
s = str.dup
|
116
|
+
until s.empty?
|
117
|
+
(ret ||= []).push s.slice!(0, 7)
|
118
|
+
end
|
119
|
+
ret
|
120
|
+
end
|
121
|
+
|
122
|
+
def gen_keys(str)
|
123
|
+
split7(str).map{ |str7|
|
124
|
+
bits = split7(str7.unpack("B*")[0]).inject('')\
|
125
|
+
{|ret, tkn| ret += tkn + (tkn.gsub('1', '').size % 2).to_s }
|
126
|
+
[bits].pack("B*")
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def apply_des(plain, keys)
|
131
|
+
dec = OpenSSL::Cipher::DES.new
|
132
|
+
keys.map {|k|
|
133
|
+
dec.key = k
|
134
|
+
dec.encrypt.update(plain)
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def lm_hash(password)
|
139
|
+
keys = gen_keys password.upcase.ljust(14, "\0")
|
140
|
+
apply_des(LM_MAGIC, keys).join
|
141
|
+
end
|
142
|
+
|
143
|
+
def ntlm_hash(password, opt = {})
|
144
|
+
pwd = password.dup
|
145
|
+
unless opt[:unicode]
|
146
|
+
pwd = encode_utf16le(pwd)
|
147
|
+
end
|
148
|
+
OpenSSL::Digest::MD4.digest pwd
|
149
|
+
end
|
150
|
+
|
151
|
+
def ntlmv2_hash(user, password, target, opt={})
|
152
|
+
ntlmhash = ntlm_hash(password, opt)
|
153
|
+
userdomain = (user + target).upcase
|
154
|
+
unless opt[:unicode]
|
155
|
+
userdomain = encode_utf16le(userdomain)
|
156
|
+
end
|
157
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain)
|
158
|
+
end
|
159
|
+
|
160
|
+
# responses
|
161
|
+
def lm_response(arg)
|
162
|
+
begin
|
163
|
+
hash = arg[:lm_hash]
|
164
|
+
chal = arg[:challenge]
|
165
|
+
rescue
|
166
|
+
raise ArgumentError
|
167
|
+
end
|
168
|
+
chal = NTL::pack_int64le(chal) if chal.is_a?(Integer)
|
169
|
+
keys = gen_keys hash.ljust(21, "\0")
|
170
|
+
apply_des(chal, keys).join
|
171
|
+
end
|
172
|
+
|
173
|
+
def ntlm_response(arg)
|
174
|
+
hash = arg[:ntlm_hash]
|
175
|
+
chal = arg[:challenge]
|
176
|
+
chal = NTL::pack_int64le(chal) if chal.is_a?(Integer)
|
177
|
+
keys = gen_keys hash.ljust(21, "\0")
|
178
|
+
apply_des(chal, keys).join
|
179
|
+
end
|
180
|
+
|
181
|
+
def ntlmv2_response(arg, opt = {})
|
182
|
+
begin
|
183
|
+
key = arg[:ntlmv2_hash]
|
184
|
+
chal = arg[:challenge]
|
185
|
+
ti = arg[:target_info]
|
186
|
+
rescue
|
187
|
+
raise ArgumentError
|
188
|
+
end
|
189
|
+
chal = NTL::pack_int64le(chal) if chal.is_a?(Integer)
|
190
|
+
|
191
|
+
if opt[:client_challenge]
|
192
|
+
cc = opt[:client_challenge]
|
193
|
+
else
|
194
|
+
cc = rand(MAX64)
|
195
|
+
end
|
196
|
+
cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
|
197
|
+
|
198
|
+
if opt[:timestamp]
|
199
|
+
ts = opt[:timestamp]
|
200
|
+
else
|
201
|
+
ts = Time.now.to_i
|
202
|
+
end
|
203
|
+
# epoch -> milsec from Jan 1, 1601
|
204
|
+
ts = 10000000 * (ts + TIME_OFFSET)
|
205
|
+
|
206
|
+
blob = Blob.new
|
207
|
+
blob.timestamp = ts
|
208
|
+
blob.challenge = cc
|
209
|
+
blob.target_info = ti
|
210
|
+
|
211
|
+
bb = blob.serialize
|
212
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb
|
213
|
+
end
|
214
|
+
|
215
|
+
def lmv2_response(arg, opt = {})
|
216
|
+
key = arg[:ntlmv2_hash]
|
217
|
+
chal = arg[:challenge]
|
218
|
+
|
219
|
+
chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer)
|
220
|
+
|
221
|
+
if opt[:client_challenge]
|
222
|
+
cc = opt[:client_challenge]
|
223
|
+
else
|
224
|
+
cc = rand(MAX64)
|
225
|
+
end
|
226
|
+
cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
|
227
|
+
|
228
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + cc) + cc
|
229
|
+
end
|
230
|
+
|
231
|
+
def ntlm2_session(arg, opt = {})
|
232
|
+
begin
|
233
|
+
passwd_hash = arg[:ntlm_hash]
|
234
|
+
chal = arg[:challenge]
|
235
|
+
rescue
|
236
|
+
raise ArgumentError
|
237
|
+
end
|
238
|
+
|
239
|
+
if opt[:client_challenge]
|
240
|
+
cc = opt[:client_challenge]
|
241
|
+
else
|
242
|
+
cc = rand(MAX64)
|
243
|
+
end
|
244
|
+
cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
|
245
|
+
|
246
|
+
keys = gen_keys passwd_hash.ljust(21, "\0")
|
247
|
+
session_hash = OpenSSL::Digest::MD5.digest(chal + cc).slice(0, 8)
|
248
|
+
response = apply_des(session_hash, keys).join
|
249
|
+
[cc.ljust(24, "\0"), response]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
|
254
|
+
# base classes for primitives
|
255
|
+
class Field
|
256
|
+
attr_accessor :active, :value
|
257
|
+
|
258
|
+
def initialize(opts)
|
259
|
+
@value = opts[:value]
|
260
|
+
@active = opts[:active].nil? ? true : opts[:active]
|
261
|
+
end
|
262
|
+
|
263
|
+
def size
|
264
|
+
@active ? @size : 0
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class String < Field
|
269
|
+
def initialize(opts)
|
270
|
+
super(opts)
|
271
|
+
@size = opts[:size]
|
272
|
+
end
|
273
|
+
|
274
|
+
def parse(str, offset=0)
|
275
|
+
if @active and str.size >= offset + @size
|
276
|
+
@value = str[offset, @size]
|
277
|
+
@size
|
278
|
+
else
|
279
|
+
0
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def serialize
|
284
|
+
if @active
|
285
|
+
@value
|
286
|
+
else
|
287
|
+
""
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def value=(val)
|
292
|
+
@value = val
|
293
|
+
@size = @value.nil? ? 0 : @value.size
|
294
|
+
@active = (@size > 0)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
class Int16LE < Field
|
300
|
+
def initialize(opt)
|
301
|
+
super(opt)
|
302
|
+
@size = 2
|
303
|
+
end
|
304
|
+
def parse(str, offset=0)
|
305
|
+
if @active and str.size >= offset + @size
|
306
|
+
@value = str[offset, @size].unpack("v")[0]
|
307
|
+
@size
|
308
|
+
else
|
309
|
+
0
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def serialize
|
314
|
+
[@value].pack("v")
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
class Int32LE < Field
|
319
|
+
def initialize(opt)
|
320
|
+
super(opt)
|
321
|
+
@size = 4
|
322
|
+
end
|
323
|
+
|
324
|
+
def parse(str, offset=0)
|
325
|
+
if @active and str.size >= offset + @size
|
326
|
+
@value = str.slice(offset, @size).unpack("V")[0]
|
327
|
+
@size
|
328
|
+
else
|
329
|
+
0
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def serialize
|
334
|
+
[@value].pack("V") if @active
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
class Int64LE < Field
|
339
|
+
def initialize(opt)
|
340
|
+
super(opt)
|
341
|
+
@size = 8
|
342
|
+
end
|
343
|
+
|
344
|
+
def parse(str, offset=0)
|
345
|
+
if @active and str.size >= offset + @size
|
346
|
+
d, u = str.slice(offset, @size).unpack("V2")
|
347
|
+
@value = (u * 0x100000000 + d)
|
348
|
+
@size
|
349
|
+
else
|
350
|
+
0
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def serialize
|
355
|
+
[@value & 0x00000000ffffffff, @value >> 32].pack("V2") if @active
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# base class of data structure
|
360
|
+
class FieldSet
|
361
|
+
class << FieldSet
|
362
|
+
def define(&block)
|
363
|
+
c = Class.new(self)
|
364
|
+
def c.inherited(subclass)
|
365
|
+
proto = @proto
|
366
|
+
subclass.instance_eval {
|
367
|
+
@proto = proto
|
368
|
+
}
|
369
|
+
end
|
370
|
+
c.module_eval(&block)
|
371
|
+
c
|
372
|
+
end
|
373
|
+
|
374
|
+
def string(name, opts)
|
375
|
+
add_field(name, String, opts)
|
376
|
+
end
|
377
|
+
|
378
|
+
def int16LE(name, opts)
|
379
|
+
add_field(name, Int16LE, opts)
|
380
|
+
end
|
381
|
+
|
382
|
+
def int32LE(name, opts)
|
383
|
+
add_field(name, Int32LE, opts)
|
384
|
+
end
|
385
|
+
|
386
|
+
def int64LE(name, opts)
|
387
|
+
add_field(name, Int64LE, opts)
|
388
|
+
end
|
389
|
+
|
390
|
+
def security_buffer(name, opts)
|
391
|
+
add_field(name, SecurityBuffer, opts)
|
392
|
+
end
|
393
|
+
|
394
|
+
def prototypes
|
395
|
+
@proto
|
396
|
+
end
|
397
|
+
|
398
|
+
def names
|
399
|
+
@proto.map{|n, t, o| n}
|
400
|
+
end
|
401
|
+
|
402
|
+
def types
|
403
|
+
@proto.map{|n, t, o| t}
|
404
|
+
end
|
405
|
+
|
406
|
+
def opts
|
407
|
+
@proto.map{|n, t, o| o}
|
408
|
+
end
|
409
|
+
|
410
|
+
private
|
411
|
+
|
412
|
+
def add_field(name, type, opts)
|
413
|
+
(@proto ||= []).push [name, type, opts]
|
414
|
+
define_accessor name
|
415
|
+
end
|
416
|
+
|
417
|
+
def define_accessor(name)
|
418
|
+
module_eval(<<-End, __FILE__, __LINE__ + 1)
|
419
|
+
def #{name}
|
420
|
+
self['#{name}'].value
|
421
|
+
end
|
422
|
+
|
423
|
+
def #{name}=(val)
|
424
|
+
self['#{name}'].value = val
|
425
|
+
end
|
426
|
+
End
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def initialize
|
431
|
+
@alist = self.class.prototypes.map{ |n, t, o| [n, t.new(o)] }
|
432
|
+
end
|
433
|
+
|
434
|
+
def serialize
|
435
|
+
@alist.map{|n, f| f.serialize }.join
|
436
|
+
end
|
437
|
+
|
438
|
+
def parse(str, offset=0)
|
439
|
+
@alist.inject(offset){|cur, a| cur += a[1].parse(str, cur)}
|
440
|
+
end
|
441
|
+
|
442
|
+
def size
|
443
|
+
@alist.inject(0){|sum, a| sum += a[1].size}
|
444
|
+
end
|
445
|
+
|
446
|
+
def [](name)
|
447
|
+
a = @alist.assoc(name.to_s.intern)
|
448
|
+
raise ArgumentError, "no such field: #{name}" unless a
|
449
|
+
a[1]
|
450
|
+
end
|
451
|
+
|
452
|
+
def []=(name, val)
|
453
|
+
a = @alist.assoc(name.to_s.intern)
|
454
|
+
raise ArgumentError, "no such field: #{name}" unless a
|
455
|
+
a[1] = val
|
456
|
+
end
|
457
|
+
|
458
|
+
def enable(name)
|
459
|
+
self[name].active = true
|
460
|
+
end
|
461
|
+
|
462
|
+
def disable(name)
|
463
|
+
self[name].active = false
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
Blob = FieldSet.define {
|
469
|
+
int32LE :blob_signature, {:value => BLOB_SIGN}
|
470
|
+
int32LE :reserved, {:value => 0}
|
471
|
+
int64LE :timestamp, {:value => 0}
|
472
|
+
string :challenge, {:value => "", :size => 8}
|
473
|
+
int32LE :unknown1, {:value => 0}
|
474
|
+
string :target_info, {:value => "", :size => 0}
|
475
|
+
int32LE :unknown2, {:value => 0}
|
476
|
+
}
|
477
|
+
|
478
|
+
SecurityBuffer = FieldSet.define {
|
479
|
+
int16LE :length, {:value => 0}
|
480
|
+
int16LE :allocated, {:value => 0}
|
481
|
+
int32LE :offset, {:value => 0}
|
482
|
+
}
|
483
|
+
|
484
|
+
class SecurityBuffer
|
485
|
+
attr_accessor :active
|
486
|
+
def initialize(opts)
|
487
|
+
super()
|
488
|
+
@value = opts[:value]
|
489
|
+
@active = opts[:active].nil? ? true : opts[:active]
|
490
|
+
@size = 8
|
491
|
+
end
|
492
|
+
|
493
|
+
def parse(str, offset=0)
|
494
|
+
if @active and str.size >= offset + @size
|
495
|
+
super(str, offset)
|
496
|
+
@value = str[self.offset, self.length]
|
497
|
+
@size
|
498
|
+
else
|
499
|
+
0
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def serialize
|
504
|
+
super if @active
|
505
|
+
end
|
506
|
+
|
507
|
+
def value
|
508
|
+
@value
|
509
|
+
end
|
510
|
+
|
511
|
+
def value=(val)
|
512
|
+
@value = val
|
513
|
+
self.length = self.allocated = val.size
|
514
|
+
end
|
515
|
+
|
516
|
+
def data_size
|
517
|
+
@active ? @value.size : 0
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
class Message < FieldSet
|
522
|
+
class << Message
|
523
|
+
def parse(str)
|
524
|
+
m = Type0.new
|
525
|
+
m.parse(str)
|
526
|
+
case m.type
|
527
|
+
when 1
|
528
|
+
t = Type1.parse(str)
|
529
|
+
when 2
|
530
|
+
t = Type2.parse(str)
|
531
|
+
when 3
|
532
|
+
t = Type3.parse(str)
|
533
|
+
else
|
534
|
+
raise ArgumentError, "unknown type: #{m.type}"
|
535
|
+
end
|
536
|
+
t
|
537
|
+
end
|
538
|
+
|
539
|
+
def decode64(str)
|
540
|
+
parse(Base64.decode64(str))
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
def has_flag?(flag)
|
545
|
+
(self[:flag].value & FLAGS[flag]) == FLAGS[flag]
|
546
|
+
end
|
547
|
+
|
548
|
+
def set_flag(flag)
|
549
|
+
self[:flag].value |= FLAGS[flag]
|
550
|
+
end
|
551
|
+
|
552
|
+
def dump_flags
|
553
|
+
FLAG_KEYS.each{ |k| print(k, "=", flag?(k), "\n") }
|
554
|
+
end
|
555
|
+
|
556
|
+
def serialize
|
557
|
+
deflag
|
558
|
+
super + security_buffers.map{|n, f| f.value}.join
|
559
|
+
end
|
560
|
+
|
561
|
+
def encode64
|
562
|
+
Base64.encode64(serialize).gsub(/\n/, '')
|
563
|
+
end
|
564
|
+
|
565
|
+
def decode64(str)
|
566
|
+
parse(Base64.decode64(str))
|
567
|
+
end
|
568
|
+
|
569
|
+
alias head_size size
|
570
|
+
|
571
|
+
def data_size
|
572
|
+
security_buffers.inject(0){|sum, a| sum += a[1].data_size}
|
573
|
+
end
|
574
|
+
|
575
|
+
def size
|
576
|
+
head_size + data_size
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
private
|
581
|
+
|
582
|
+
def security_buffers
|
583
|
+
@alist.find_all{|n, f| f.instance_of?(SecurityBuffer)}
|
584
|
+
end
|
585
|
+
|
586
|
+
def deflag
|
587
|
+
security_buffers.inject(head_size){|cur, a|
|
588
|
+
a[1].offset = cur
|
589
|
+
cur += a[1].data_size
|
590
|
+
}
|
591
|
+
end
|
592
|
+
|
593
|
+
def data_edge
|
594
|
+
security_buffers.map{ |n, f| f.active ? f.offset : size}.min
|
595
|
+
end
|
596
|
+
|
597
|
+
# sub class definitions
|
598
|
+
|
599
|
+
Type0 = Message.define {
|
600
|
+
string :sign, {:size => 8, :value => SSP_SIGN}
|
601
|
+
int32LE :type, {:value => 0}
|
602
|
+
}
|
603
|
+
|
604
|
+
Type1 = Message.define {
|
605
|
+
string :sign, {:size => 8, :value => SSP_SIGN}
|
606
|
+
int32LE :type, {:value => 1}
|
607
|
+
int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE1] }
|
608
|
+
security_buffer :domain, {:value => "", :active => false}
|
609
|
+
security_buffer :workstation, {:value => "", :active => false}
|
610
|
+
string :padding, {:size => 0, :value => "", :active => false }
|
611
|
+
}
|
612
|
+
|
613
|
+
class Type1
|
614
|
+
class << Type1
|
615
|
+
def parse(str)
|
616
|
+
t = new
|
617
|
+
t.parse(str)
|
618
|
+
t
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
def parse(str)
|
623
|
+
super(str)
|
624
|
+
enable(:domain) if has_flag?(:DOMAIN_SUPPLIED)
|
625
|
+
enable(:workstation) if has_flag?(:WORKSTATION_SUPPLIED)
|
626
|
+
super(str)
|
627
|
+
if ( (len = data_edge - head_size) > 0)
|
628
|
+
self.padding = "\0" * len
|
629
|
+
super(str)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
Type2 = Message.define{
|
635
|
+
string :sign, {:size => 8, :value => SSP_SIGN}
|
636
|
+
int32LE :type, {:value => 2}
|
637
|
+
security_buffer :target_name, {:size => 0, :value => ""}
|
638
|
+
int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE2]}
|
639
|
+
int64LE :challenge, {:value => 0}
|
640
|
+
int64LE :context, {:value => 0, :active => false}
|
641
|
+
security_buffer :target_info, {:value => "", :active => false}
|
642
|
+
string :padding, {:size => 0, :value => "", :active => false }
|
643
|
+
}
|
644
|
+
|
645
|
+
class Type2
|
646
|
+
class << Type2
|
647
|
+
def parse(str)
|
648
|
+
t = new
|
649
|
+
t.parse(str)
|
650
|
+
t
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
def parse(str)
|
655
|
+
super(str)
|
656
|
+
if has_flag?(:TARGET_INFO)
|
657
|
+
enable(:context)
|
658
|
+
enable(:target_info)
|
659
|
+
super(str)
|
660
|
+
end
|
661
|
+
if ( (len = data_edge - head_size) > 0)
|
662
|
+
self.padding = "\0" * len
|
663
|
+
super(str)
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
def response(arg, opt = {})
|
668
|
+
usr = arg[:user]
|
669
|
+
pwd = arg[:password]
|
670
|
+
if usr.nil? or pwd.nil?
|
671
|
+
raise ArgumentError, "user and password have to be supplied"
|
672
|
+
end
|
673
|
+
|
674
|
+
if opt[:workstation]
|
675
|
+
ws = opt[:workstation]
|
676
|
+
else
|
677
|
+
ws = ""
|
678
|
+
end
|
679
|
+
|
680
|
+
if opt[:client_challenge]
|
681
|
+
cc = opt[:client_challenge]
|
682
|
+
else
|
683
|
+
cc = rand(MAX64)
|
684
|
+
end
|
685
|
+
cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer)
|
686
|
+
opt[:client_challenge] = cc
|
687
|
+
|
688
|
+
if has_flag?(:OEM) and opt[:unicode]
|
689
|
+
usr = NTLM::decode_utf16le(usr)
|
690
|
+
pwd = NTLM::decode_utf16le(pwd)
|
691
|
+
ws = NTLM::decode_utf16le(ws)
|
692
|
+
opt[:unicode] = false
|
693
|
+
end
|
694
|
+
|
695
|
+
if has_flag?(:UNICODE) and !opt[:unicode]
|
696
|
+
usr = NTLM::encode_utf16le(usr)
|
697
|
+
pwd = NTLM::encode_utf16le(pwd)
|
698
|
+
ws = NTLM::encode_utf16le(ws)
|
699
|
+
opt[:unicode] = true
|
700
|
+
end
|
701
|
+
|
702
|
+
tgt = self.target_name
|
703
|
+
ti = self.target_info
|
704
|
+
|
705
|
+
chal = self[:challenge].serialize
|
706
|
+
|
707
|
+
if opt[:ntlmv2]
|
708
|
+
ar = {:ntlmv2_hash => NTLM::ntlmv2_hash(usr, pwd, tgt, opt), :challenge => chal, :target_info => ti}
|
709
|
+
lm_res = NTLM::lmv2_response(ar, opt)
|
710
|
+
ntlm_res = NTLM::ntlmv2_response(ar, opt)
|
711
|
+
elsif has_flag?(:NTLM2_KEY)
|
712
|
+
ar = {:ntlm_hash => NTLM::ntlm_hash(pwd, opt), :challenge => chal}
|
713
|
+
lm_res, ntlm_res = NTLM::ntlm2_session(ar, opt)
|
714
|
+
else
|
715
|
+
lm_res = NTLM::lm_response(pwd, chal)
|
716
|
+
ntlm_res = NTLM::ntlm_response(pwd, chal)
|
717
|
+
end
|
718
|
+
|
719
|
+
Type3.create({
|
720
|
+
:lm_response => lm_res,
|
721
|
+
:ntlm_response => ntlm_res,
|
722
|
+
:domain => tgt,
|
723
|
+
:user => usr,
|
724
|
+
:workstation => ws,
|
725
|
+
:flag => self.flag
|
726
|
+
})
|
727
|
+
end
|
728
|
+
end
|
729
|
+
|
730
|
+
|
731
|
+
Type3 = Message.define{
|
732
|
+
string :sign, {:size => 8, :value => SSP_SIGN}
|
733
|
+
int32LE :type, {:value => 3}
|
734
|
+
security_buffer :lm_response, {:value => ""}
|
735
|
+
security_buffer :ntlm_response, {:value => ""}
|
736
|
+
security_buffer :domain, {:value => ""}
|
737
|
+
security_buffer :user, {:value => ""}
|
738
|
+
security_buffer :workstation, {:value => ""}
|
739
|
+
security_buffer :session_key, {:value => "", :active => false }
|
740
|
+
int64LE :flag, {:value => 0, :active => false }
|
741
|
+
}
|
742
|
+
|
743
|
+
class Type3
|
744
|
+
class << Type3
|
745
|
+
def parse(str)
|
746
|
+
t = new
|
747
|
+
t.parse(str)
|
748
|
+
t
|
749
|
+
end
|
750
|
+
|
751
|
+
def create(arg, opt ={})
|
752
|
+
t = new
|
753
|
+
t.lm_response = arg[:lm_response]
|
754
|
+
t.ntlm_response = arg[:ntlm_response]
|
755
|
+
t.domain = arg[:domain]
|
756
|
+
t.user = arg[:user]
|
757
|
+
t.workstation = arg[:workstation]
|
758
|
+
|
759
|
+
if arg[:session_key]
|
760
|
+
t.enable(:session_key)
|
761
|
+
t.session_key = arg[session_key]
|
762
|
+
end
|
763
|
+
if arg[:flag]
|
764
|
+
t.enable(:session_key)
|
765
|
+
t.enable(:flag)
|
766
|
+
t.flag = arg[:flag]
|
767
|
+
end
|
768
|
+
t
|
769
|
+
end
|
770
|
+
end
|
771
|
+
end
|
772
|
+
end
|
773
|
+
end
|
774
|
+
end
|