lib/net/ntlm.rb in rubyntlm-0.3.1 vs lib/net/ntlm.rb in rubyntlm-0.3.2

- old
+ new

@@ -1,845 +1,869 @@ -# encoding: UTF-8 -# -# = net/ntlm.rb -# -# An NTLM Authentication Library for Ruby -# -# This code is a derivative of "dbf2.rb" written by yrock -# and Minero Aoki. You can find original code here: -# http://jp.rubyist.net/magazine/?0013-CodeReview -# ------------------------------------------------------------- -# Copyright (c) 2005,2006 yrock -# -# This program is free software. -# You can distribute/modify this program under the terms of the -# Ruby License. -# -# 2006-02-11 refactored by Minero Aoki -# ------------------------------------------------------------- -# -# All protocol information used to write this code stems from -# "The NTLM Authentication Protocol" by Eric Glass. The author -# would thank to him for this tremendous work and making it -# available on the net. -# http://davenport.sourceforge.net/ntlm.html -# ------------------------------------------------------------- -# Copyright (c) 2003 Eric Glass -# -# Permission to use, copy, modify, and distribute this document -# for any purpose and without any fee is hereby granted, -# provided that the above copyright notice and this list of -# conditions appear in all copies. -# ------------------------------------------------------------- -# -# The author also looked Mozilla-Firefox-1.0.7 source code, -# namely, security/manager/ssl/src/nsNTLMAuthModule.cpp and -# Jonathan Bastien-Filiatrault's libntlm-ruby. -# "http://x2a.org/websvn/filedetails.php? -# repname=libntlm-ruby&path=%2Ftrunk%2Fntlm.rb&sc=1" -# The latter has a minor bug in its separate_keys function. -# The third key has to begin from the 14th character of the -# input string instead of 13th:) -#-- -# $Id: ntlm.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $ -#++ - -require 'base64' -require 'openssl' -require 'openssl/digest' -require 'socket' - -module Net - module NTLM - # @private - module VERSION - MAJOR = 0 - MINOR = 3 - TINY = 1 - STRING = [MAJOR, MINOR, TINY].join('.') - end - - SSP_SIGN = "NTLMSSP\0" - BLOB_SIGN = 0x00000101 - LM_MAGIC = "KGS!@\#$%" - TIME_OFFSET = 11644473600 - MAX64 = 0xffffffffffffffff - - FLAGS = { - :UNICODE => 0x00000001, - :OEM => 0x00000002, - :REQUEST_TARGET => 0x00000004, - :MBZ9 => 0x00000008, - :SIGN => 0x00000010, - :SEAL => 0x00000020, - :NEG_DATAGRAM => 0x00000040, - :NETWARE => 0x00000100, - :NTLM => 0x00000200, - :NEG_NT_ONLY => 0x00000400, - :MBZ7 => 0x00000800, - :DOMAIN_SUPPLIED => 0x00001000, - :WORKSTATION_SUPPLIED => 0x00002000, - :LOCAL_CALL => 0x00004000, - :ALWAYS_SIGN => 0x00008000, - :TARGET_TYPE_DOMAIN => 0x00010000, - :TARGET_INFO => 0x00800000, - :NTLM2_KEY => 0x00080000, - :KEY128 => 0x20000000, - :KEY56 => 0x80000000 - }.freeze - - FLAG_KEYS = FLAGS.keys.sort{|a, b| FLAGS[a] <=> FLAGS[b] } - - DEFAULT_FLAGS = { - :TYPE1 => FLAGS[:UNICODE] | FLAGS[:OEM] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY], - :TYPE2 => FLAGS[:UNICODE], - :TYPE3 => FLAGS[:UNICODE] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY] - } - - - class << self - - # Decode a UTF16 string to a ASCII string - # @param [String] str The string to convert - def decode_utf16le(str) - str.encode(Encoding::UTF_8, Encoding::UTF_16LE).force_encoding('UTF-8') - end - - # Encodes a ASCII string to a UTF16 string - # @param [String] str The string to convert - # @note This implementation may seem stupid but the problem is that UTF16-LE and UTF-8 are incompatiable - # encodings. This library uses string contatination to build the packet bytes. The end result is that - # you can either marshal the encodings elsewhere of simply know that each time you call encode_utf16le - # the function will convert the string bytes to UTF-16LE and note the encoding as UTF-8 so that byte - # concatination works seamlessly. - def encode_utf16le(str) - str = str.force_encoding('UTF-8') if [::Encoding::ASCII_8BIT,::Encoding::US_ASCII].include?(str.encoding) - str.force_encoding('UTF-8').encode(Encoding::UTF_16LE, Encoding::UTF_8).force_encoding('UTF-8') - end - - # Conver the value to a 64-Bit Little Endian Int - # @param [String] val The string to convert - def pack_int64le(val) - [val & 0x00000000ffffffff, val >> 32].pack("V2") - end - - # Builds an array of strings that are 7 characters long - # @param [String] str The string to split - # @api private - def split7(str) - s = str.dup - until s.empty? - (ret ||= []).push s.slice!(0, 7) - end - ret - end - - # Not sure what this is doing - # @param [String] str String to generate keys for - # @api private - def gen_keys(str) - split7(str).map{ |str7| - bits = split7(str7.unpack("B*")[0]).inject('')\ - {|ret, tkn| ret += tkn + (tkn.gsub('1', '').size % 2).to_s } - [bits].pack("B*") - } - end - - def apply_des(plain, keys) - dec = OpenSSL::Cipher::DES.new - keys.map {|k| - dec.key = k - dec.encrypt.update(plain) - } - end - - # Generates a Lan Manager Hash - # @param [String] password The password to base the hash on - def lm_hash(password) - keys = gen_keys password.upcase.ljust(14, "\0") - apply_des(LM_MAGIC, keys).join - end - - # Generate a NTLM Hash - # @param [String] password The password to base the hash on - # @option opt :unicode (false) Unicode encode the password - def ntlm_hash(password, opt = {}) - pwd = password.dup - unless opt[:unicode] - pwd = encode_utf16le(pwd) - end - OpenSSL::Digest::MD4.digest pwd - end - - # Generate a NTLMv2 Hash - # @param [String] user The username - # @param [String] password The password - # @param [String] target The domain or workstaiton to authenticate to - # @option opt :unicode (false) Unicode encode the domain - def ntlmv2_hash(user, password, target, opt={}) - ntlmhash = ntlm_hash(password, opt) - userdomain = (user + target).upcase - unless opt[:unicode] - userdomain = encode_utf16le(userdomain) - end - OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain) - end - - def lm_response(arg) - begin - hash = arg[:lm_hash] - chal = arg[:challenge] - rescue - raise ArgumentError - end - chal = NTL::pack_int64le(chal) if chal.is_a?(Integer) - keys = gen_keys hash.ljust(21, "\0") - apply_des(chal, keys).join - end - - def ntlm_response(arg) - hash = arg[:ntlm_hash] - chal = arg[:challenge] - chal = NTL::pack_int64le(chal) if chal.is_a?(Integer) - keys = gen_keys hash.ljust(21, "\0") - apply_des(chal, keys).join - end - - def ntlmv2_response(arg, opt = {}) - begin - key = arg[:ntlmv2_hash] - chal = arg[:challenge] - ti = arg[:target_info] - rescue - raise ArgumentError - end - chal = NTL::pack_int64le(chal) if chal.is_a?(Integer) - - if opt[:client_challenge] - cc = opt[:client_challenge] - else - cc = rand(MAX64) - end - cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) - - if opt[:timestamp] - ts = opt[:timestamp] - else - ts = Time.now.to_i - end - # epoch -> milsec from Jan 1, 1601 - ts = 10000000 * (ts + TIME_OFFSET) - - blob = Blob.new - blob.timestamp = ts - blob.challenge = cc - blob.target_info = ti - - bb = blob.serialize - OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb - end - - def lmv2_response(arg, opt = {}) - key = arg[:ntlmv2_hash] - chal = arg[:challenge] - - chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) - - if opt[:client_challenge] - cc = opt[:client_challenge] - else - cc = rand(MAX64) - end - cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) - - OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + cc) + cc - end - - def ntlm2_session(arg, opt = {}) - begin - passwd_hash = arg[:ntlm_hash] - chal = arg[:challenge] - rescue - raise ArgumentError - end - - if opt[:client_challenge] - cc = opt[:client_challenge] - else - cc = rand(MAX64) - end - cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) - - keys = gen_keys passwd_hash.ljust(21, "\0") - session_hash = OpenSSL::Digest::MD5.digest(chal + cc).slice(0, 8) - response = apply_des(session_hash, keys).join - [cc.ljust(24, "\0"), response] - end - end - - - # base classes for primitives - # @private - class Field - attr_accessor :active, :value - - def initialize(opts) - @value = opts[:value] - @active = opts[:active].nil? ? true : opts[:active] - end - - def size - @active ? @size : 0 - end - end - - class String < Field - def initialize(opts) - super(opts) - @size = opts[:size] - end - - def parse(str, offset=0) - if @active and str.size >= offset + @size - @value = str[offset, @size] - @size - else - 0 - end - end - - def serialize - if @active - @value - else - "" - end - end - - def value=(val) - @value = val - @size = @value.nil? ? 0 : @value.size - @active = (@size > 0) - end - end - - class Int16LE < Field - def initialize(opt) - super(opt) - @size = 2 - end - def parse(str, offset=0) - if @active and str.size >= offset + @size - @value = str[offset, @size].unpack("v")[0] - @size - else - 0 - end - end - - def serialize - [@value].pack("v") - end - end - - class Int32LE < Field - def initialize(opt) - super(opt) - @size = 4 - end - - def parse(str, offset=0) - if @active and str.size >= offset + @size - @value = str.slice(offset, @size).unpack("V")[0] - @size - else - 0 - end - end - - def serialize - [@value].pack("V") if @active - end - end - - class Int64LE < Field - def initialize(opt) - super(opt) - @size = 8 - end - - def parse(str, offset=0) - if @active and str.size >= offset + @size - d, u = str.slice(offset, @size).unpack("V2") - @value = (u * 0x100000000 + d) - @size - else - 0 - end - end - - def serialize - [@value & 0x00000000ffffffff, @value >> 32].pack("V2") if @active - end - end - - # base class of data structure - class FieldSet - class << FieldSet - - - # @macro string_security_buffer - # @method $1 - # @method $1= - # @return [String] - def string(name, opts) - add_field(name, String, opts) - end - - # @macro int16le_security_buffer - # @method $1 - # @method $1= - # @return [Int16LE] - def int16LE(name, opts) - add_field(name, Int16LE, opts) - end - - # @macro int32le_security_buffer - # @method $1 - # @method $1= - # @return [Int32LE] - def int32LE(name, opts) - add_field(name, Int32LE, opts) - end - - # @macro int64le_security_buffer - # @method $1 - # @method $1= - # @return [Int64] - def int64LE(name, opts) - add_field(name, Int64LE, opts) - end - - # @macro security_buffer - # @method $1 - # @method $1= - # @return [SecurityBuffer] - def security_buffer(name, opts) - add_field(name, SecurityBuffer, opts) - end - - def prototypes - @proto - end - - def names - @proto.map{|n, t, o| n} - end - - def types - @proto.map{|n, t, o| t} - end - - def opts - @proto.map{|n, t, o| o} - end - - private - - def add_field(name, type, opts) - (@proto ||= []).push [name, type, opts] - define_accessor name - end - - def define_accessor(name) - module_eval(<<-End, __FILE__, __LINE__ + 1) - def #{name} - self['#{name}'].value - end - - def #{name}=(val) - self['#{name}'].value = val - end - End - end - end - - def initialize - @alist = self.class.prototypes.map{ |n, t, o| [n, t.new(o)] } - end - - def serialize - @alist.map{|n, f| f.serialize }.join - end - - def parse(str, offset=0) - @alist.inject(offset){|cur, a| cur += a[1].parse(str, cur)} - end - - def size - @alist.inject(0){|sum, a| sum += a[1].size} - end - - def [](name) - a = @alist.assoc(name.to_s.intern) - raise ArgumentError, "no such field: #{name}" unless a - a[1] - end - - def []=(name, val) - a = @alist.assoc(name.to_s.intern) - raise ArgumentError, "no such field: #{name}" unless a - a[1] = val - end - - def enable(name) - self[name].active = true - end - - def disable(name) - self[name].active = false - end - end - - class Blob < FieldSet - int32LE :blob_signature, {:value => BLOB_SIGN} - int32LE :reserved, {:value => 0} - int64LE :timestamp, {:value => 0} - string :challenge, {:value => "", :size => 8} - int32LE :unknown1, {:value => 0} - string :target_info, {:value => "", :size => 0} - int32LE :unknown2, {:value => 0} - end - - class SecurityBuffer < FieldSet - - int16LE :length, {:value => 0} - int16LE :allocated, {:value => 0} - int32LE :offset, {:value => 0} - - attr_accessor :active - def initialize(opts) - super() - @value = opts[:value] - @active = opts[:active].nil? ? true : opts[:active] - @size = 8 - end - - def parse(str, offset=0) - if @active and str.size >= offset + @size - super(str, offset) - @value = str[self.offset, self.length] - @size - else - 0 - end - end - - def serialize - super if @active - end - - def value - @value - end - - def value=(val) - @value = val - self.length = self.allocated = val.size - end - - def data_size - @active ? @value.size : 0 - end - end - - # @private false - class Message < FieldSet - class << Message - def parse(str) - m = Type0.new - m.parse(str) - case m.type - when 1 - t = Type1.parse(str) - when 2 - t = Type2.parse(str) - when 3 - t = Type3.parse(str) - else - raise ArgumentError, "unknown type: #{m.type}" - end - t - end - - def decode64(str) - parse(Base64.decode64(str)) - end - end - - def has_flag?(flag) - (self[:flag].value & FLAGS[flag]) == FLAGS[flag] - end - - def set_flag(flag) - self[:flag].value |= FLAGS[flag] - end - - def dump_flags - FLAG_KEYS.each{ |k| print(k, "=", flag?(k), "\n") } - end - - def serialize - deflag - super + security_buffers.map{|n, f| f.value}.join - end - - def encode64 - Base64.encode64(serialize).gsub(/\n/, '') - end - - def decode64(str) - parse(Base64.decode64(str)) - end - - alias head_size size - - def data_size - security_buffers.inject(0){|sum, a| sum += a[1].data_size} - end - - def size - head_size + data_size - end - - - def security_buffers - @alist.find_all{|n, f| f.instance_of?(SecurityBuffer)} - end - - def deflag - security_buffers.inject(head_size){|cur, a| - a[1].offset = cur - cur += a[1].data_size - } - end - - def data_edge - security_buffers.map{ |n, f| f.active ? f.offset : size}.min - end - - # sub class definitions - class Type0 < Message - string :sign, {:size => 8, :value => SSP_SIGN} - int32LE :type, {:value => 0} - end - - # @private false - class Type1 < Message - - string :sign, {:size => 8, :value => SSP_SIGN} - int32LE :type, {:value => 1} - int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE1] } - security_buffer :domain, {:value => ""} - security_buffer :workstation, {:value => Socket.gethostname } - string :padding, {:size => 0, :value => "", :active => false } - - class << Type1 - # Parses a Type 1 Message - # @param [String] str A string containing Type 1 data - # @return [Type1] The parsed Type 1 message - def parse(str) - t = new - t.parse(str) - t - end - end - - # @!visibility private - def parse(str) - super(str) - enable(:domain) if has_flag?(:DOMAIN_SUPPLIED) - enable(:workstation) if has_flag?(:WORKSTATION_SUPPLIED) - super(str) - if ( (len = data_edge - head_size) > 0) - self.padding = "\0" * len - super(str) - end - end - end - - - # @private false - class Type2 < Message - - string :sign, {:size => 8, :value => SSP_SIGN} - int32LE :type, {:value => 2} - security_buffer :target_name, {:size => 0, :value => ""} - int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE2]} - int64LE :challenge, {:value => 0} - int64LE :context, {:value => 0, :active => false} - security_buffer :target_info, {:value => "", :active => false} - string :padding, {:size => 0, :value => "", :active => false } - - class << Type2 - # Parse a Type 2 packet - # @param [String] str A string containing Type 2 data - # @return [Type2] - def parse(str) - t = new - t.parse(str) - t - end - end - - # @!visibility private - def parse(str) - super(str) - if has_flag?(:TARGET_INFO) - enable(:context) - enable(:target_info) - super(str) - end - if ( (len = data_edge - head_size) > 0) - self.padding = "\0" * len - super(str) - end - end - - # Generates a Type 3 response based on the Type 2 Information - # @return [Type3] - # @option arg [String] :username The username to authenticate with - # @option arg [String] :password The user's password - # @option arg [String] :domain ('') The domain to authenticate to - # @option opt [String] :workstation (Socket.gethostname) The name of the calling workstation - # @option opt [Boolean] :use_default_target (False) Use the domain supplied by the server in the Type 2 packet - # @note An empty :domain option authenticates to the local machine. - # @note The :use_default_target has presidence over the :domain option - def response(arg, opt = {}) - usr = arg[:user] - pwd = arg[:password] - domain = arg[:domain] ? arg[:domain] : "" - if usr.nil? or pwd.nil? - raise ArgumentError, "user and password have to be supplied" - end - - if opt[:workstation] - ws = opt[:workstation] - else - ws = Socket.gethostname - end - - if opt[:client_challenge] - cc = opt[:client_challenge] - else - cc = rand(MAX64) - end - cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) - opt[:client_challenge] = cc - - if has_flag?(:OEM) and opt[:unicode] - usr = NTLM::decode_utf16le(usr) - pwd = NTLM::decode_utf16le(pwd) - ws = NTLM::decode_utf16le(ws) - domain = NTLM::decode_utf16le(domain) - opt[:unicode] = false - end - - if has_flag?(:UNICODE) and !opt[:unicode] - usr = NTLM::encode_utf16le(usr) - pwd = NTLM::encode_utf16le(pwd) - ws = NTLM::encode_utf16le(ws) - domain = NTLM::encode_utf16le(domain) - opt[:unicode] = true - end - - if opt[:use_default_target] - domain = self.target_name - end - - ti = self.target_info - - chal = self[:challenge].serialize - - if opt[:ntlmv2] - ar = {:ntlmv2_hash => NTLM::ntlmv2_hash(usr, pwd, domain, opt), :challenge => chal, :target_info => ti} - lm_res = NTLM::lmv2_response(ar, opt) - ntlm_res = NTLM::ntlmv2_response(ar, opt) - elsif has_flag?(:NTLM2_KEY) - ar = {:ntlm_hash => NTLM::ntlm_hash(pwd, opt), :challenge => chal} - lm_res, ntlm_res = NTLM::ntlm2_session(ar, opt) - else - lm_res = NTLM::lm_response(pwd, chal) - ntlm_res = NTLM::ntlm_response(pwd, chal) - end - - Type3.create({ - :lm_response => lm_res, - :ntlm_response => ntlm_res, - :domain => domain, - :user => usr, - :workstation => ws, - :flag => self.flag - }) - end - end - - # @private false - class Type3 < Message - - string :sign, {:size => 8, :value => SSP_SIGN} - int32LE :type, {:value => 3} - security_buffer :lm_response, {:value => ""} - security_buffer :ntlm_response, {:value => ""} - security_buffer :domain, {:value => ""} - security_buffer :user, {:value => ""} - security_buffer :workstation, {:value => ""} - security_buffer :session_key, {:value => "", :active => false } - int64LE :flag, {:value => 0, :active => false } - - class << Type3 - # Parse a Type 3 packet - # @param [String] str A string containing Type 3 data - # @return [Type2] - def parse(str) - t = new - t.parse(str) - t - end - - # Builds a Type 3 packet - # @note All options must be properly encoded with either unicode or oem encoding - # @return [Type3] - # @option arg [String] :lm_response The LM hash - # @option arg [String] :ntlm_response The NTLM hash - # @option arg [String] :domain The domain to authenticate to - # @option arg [String] :workstation The name of the calling workstation - # @option arg [String] :session_key The session key - # @option arg [Integer] :flag Flags for the packet - def create(arg, opt ={}) - t = new - t.lm_response = arg[:lm_response] - t.ntlm_response = arg[:ntlm_response] - t.domain = arg[:domain] - t.user = arg[:user] - - if arg[:workstation] - t.workstation = arg[:workstation] - end - - if arg[:session_key] - t.enable(:session_key) - t.session_key = arg[session_key] - end - - if arg[:flag] - t.enable(:session_key) - t.enable(:flag) - t.flag = arg[:flag] - end - t - end - end - end - end - end -end +# encoding: UTF-8 +# +# = net/ntlm.rb +# +# An NTLM Authentication Library for Ruby +# +# This code is a derivative of "dbf2.rb" written by yrock +# and Minero Aoki. You can find original code here: +# http://jp.rubyist.net/magazine/?0013-CodeReview +# ------------------------------------------------------------- +# Copyright (c) 2005,2006 yrock +# +# This program is free software. +# You can distribute/modify this program under the terms of the +# Ruby License. +# +# 2006-02-11 refactored by Minero Aoki +# ------------------------------------------------------------- +# +# All protocol information used to write this code stems from +# "The NTLM Authentication Protocol" by Eric Glass. The author +# would thank to him for this tremendous work and making it +# available on the net. +# http://davenport.sourceforge.net/ntlm.html +# ------------------------------------------------------------- +# Copyright (c) 2003 Eric Glass +# +# Permission to use, copy, modify, and distribute this document +# for any purpose and without any fee is hereby granted, +# provided that the above copyright notice and this list of +# conditions appear in all copies. +# ------------------------------------------------------------- +# +# The author also looked Mozilla-Firefox-1.0.7 source code, +# namely, security/manager/ssl/src/nsNTLMAuthModule.cpp and +# Jonathan Bastien-Filiatrault's libntlm-ruby. +# "http://x2a.org/websvn/filedetails.php? +# repname=libntlm-ruby&path=%2Ftrunk%2Fntlm.rb&sc=1" +# The latter has a minor bug in its separate_keys function. +# The third key has to begin from the 14th character of the +# input string instead of 13th:) +#-- +# $Id: ntlm.rb,v 1.1 2006/10/05 01:36:52 koheik Exp $ +#++ + +require 'base64' +require 'openssl' +require 'openssl/digest' +require 'socket' + +module Net + module NTLM + # @private + module VERSION + MAJOR = 0 + MINOR = 3 + TINY = 2 + STRING = [MAJOR, MINOR, TINY].join('.') + end + + SSP_SIGN = "NTLMSSP\0" + BLOB_SIGN = 0x00000101 + LM_MAGIC = "KGS!@\#$%" + TIME_OFFSET = 11644473600 + MAX64 = 0xffffffffffffffff + + FLAGS = { + :UNICODE => 0x00000001, + :OEM => 0x00000002, + :REQUEST_TARGET => 0x00000004, + :MBZ9 => 0x00000008, + :SIGN => 0x00000010, + :SEAL => 0x00000020, + :NEG_DATAGRAM => 0x00000040, + :NETWARE => 0x00000100, + :NTLM => 0x00000200, + :NEG_NT_ONLY => 0x00000400, + :MBZ7 => 0x00000800, + :DOMAIN_SUPPLIED => 0x00001000, + :WORKSTATION_SUPPLIED => 0x00002000, + :LOCAL_CALL => 0x00004000, + :ALWAYS_SIGN => 0x00008000, + :TARGET_TYPE_DOMAIN => 0x00010000, + :TARGET_INFO => 0x00800000, + :NTLM2_KEY => 0x00080000, + :KEY128 => 0x20000000, + :KEY56 => 0x80000000 + }.freeze + + FLAG_KEYS = FLAGS.keys.sort{|a, b| FLAGS[a] <=> FLAGS[b] } + + DEFAULT_FLAGS = { + :TYPE1 => FLAGS[:UNICODE] | FLAGS[:OEM] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY], + :TYPE2 => FLAGS[:UNICODE], + :TYPE3 => FLAGS[:UNICODE] | FLAGS[:REQUEST_TARGET] | FLAGS[:NTLM] | FLAGS[:ALWAYS_SIGN] | FLAGS[:NTLM2_KEY] + } + + class EncodeUtil + if RUBY_VERSION == "1.8.7" + require "kconv" + + # Decode a UTF16 string to a ASCII string + # @param [String] str The string to convert + def self.decode_utf16le(str) + Kconv.kconv(swap16(str), Kconv::ASCII, Kconv::UTF16) + end + + # Encodes a ASCII string to a UTF16 string + # @param [String] str The string to convert + def self.encode_utf16le(str) + swap16(Kconv.kconv(str, Kconv::UTF16, Kconv::ASCII)) + end + + # Taggle the strings endianness between big/little and little/big + # @param [String] str The string to swap the endianness on + def self.swap16(str) + str.unpack("v*").pack("n*") + end + else # Use native 1.9 string encoding functions + + # Decode a UTF16 string to a ASCII string + # @param [String] str The string to convert + def self.decode_utf16le(str) + str.encode(Encoding::UTF_8, Encoding::UTF_16LE).force_encoding('UTF-8') + end + + # Encodes a ASCII string to a UTF16 string + # @param [String] str The string to convert + # @note This implementation may seem stupid but the problem is that UTF16-LE and UTF-8 are incompatiable + # encodings. This library uses string contatination to build the packet bytes. The end result is that + # you can either marshal the encodings elsewhere of simply know that each time you call encode_utf16le + # the function will convert the string bytes to UTF-16LE and note the encoding as UTF-8 so that byte + # concatination works seamlessly. + def self.encode_utf16le(str) + str = str.force_encoding('UTF-8') if [::Encoding::ASCII_8BIT,::Encoding::US_ASCII].include?(str.encoding) + str.force_encoding('UTF-8').encode(Encoding::UTF_16LE, Encoding::UTF_8).force_encoding('UTF-8') + end + end + end + + class << self + + # Conver the value to a 64-Bit Little Endian Int + # @param [String] val The string to convert + def pack_int64le(val) + [val & 0x00000000ffffffff, val >> 32].pack("V2") + end + + # Builds an array of strings that are 7 characters long + # @param [String] str The string to split + # @api private + def split7(str) + s = str.dup + until s.empty? + (ret ||= []).push s.slice!(0, 7) + end + ret + end + + # Not sure what this is doing + # @param [String] str String to generate keys for + # @api private + def gen_keys(str) + split7(str).map{ |str7| + bits = split7(str7.unpack("B*")[0]).inject('')\ + {|ret, tkn| ret += tkn + (tkn.gsub('1', '').size % 2).to_s } + [bits].pack("B*") + } + end + + def apply_des(plain, keys) + dec = OpenSSL::Cipher::DES.new + keys.map {|k| + dec.key = k + dec.encrypt.update(plain) + } + end + + # Generates a Lan Manager Hash + # @param [String] password The password to base the hash on + def lm_hash(password) + keys = gen_keys password.upcase.ljust(14, "\0") + apply_des(LM_MAGIC, keys).join + end + + # Generate a NTLM Hash + # @param [String] password The password to base the hash on + # @option opt :unicode (false) Unicode encode the password + def ntlm_hash(password, opt = {}) + pwd = password.dup + unless opt[:unicode] + pwd = EncodeUtil.encode_utf16le(pwd) + end + OpenSSL::Digest::MD4.digest pwd + end + + # Generate a NTLMv2 Hash + # @param [String] user The username + # @param [String] password The password + # @param [String] target The domain or workstaiton to authenticate to + # @option opt :unicode (false) Unicode encode the domain + def ntlmv2_hash(user, password, target, opt={}) + ntlmhash = ntlm_hash(password, opt) + userdomain = (user + target).upcase + unless opt[:unicode] + userdomain = EncodeUtil.encode_utf16le(userdomain) + end + OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain) + end + + def lm_response(arg) + begin + hash = arg[:lm_hash] + chal = arg[:challenge] + rescue + raise ArgumentError + end + chal = NTL::pack_int64le(chal) if chal.is_a?(Integer) + keys = gen_keys hash.ljust(21, "\0") + apply_des(chal, keys).join + end + + def ntlm_response(arg) + hash = arg[:ntlm_hash] + chal = arg[:challenge] + chal = NTL::pack_int64le(chal) if chal.is_a?(Integer) + keys = gen_keys hash.ljust(21, "\0") + apply_des(chal, keys).join + end + + def ntlmv2_response(arg, opt = {}) + begin + key = arg[:ntlmv2_hash] + chal = arg[:challenge] + ti = arg[:target_info] + rescue + raise ArgumentError + end + chal = NTL::pack_int64le(chal) if chal.is_a?(Integer) + + if opt[:client_challenge] + cc = opt[:client_challenge] + else + cc = rand(MAX64) + end + cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) + + if opt[:timestamp] + ts = opt[:timestamp] + else + ts = Time.now.to_i + end + # epoch -> milsec from Jan 1, 1601 + ts = 10000000 * (ts + TIME_OFFSET) + + blob = Blob.new + blob.timestamp = ts + blob.challenge = cc + blob.target_info = ti + + bb = blob.serialize + OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb + end + + def lmv2_response(arg, opt = {}) + key = arg[:ntlmv2_hash] + chal = arg[:challenge] + + chal = NTLM::pack_int64le(chal) if chal.is_a?(Integer) + + if opt[:client_challenge] + cc = opt[:client_challenge] + else + cc = rand(MAX64) + end + cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) + + OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + cc) + cc + end + + def ntlm2_session(arg, opt = {}) + begin + passwd_hash = arg[:ntlm_hash] + chal = arg[:challenge] + rescue + raise ArgumentError + end + + if opt[:client_challenge] + cc = opt[:client_challenge] + else + cc = rand(MAX64) + end + cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) + + keys = gen_keys passwd_hash.ljust(21, "\0") + session_hash = OpenSSL::Digest::MD5.digest(chal + cc).slice(0, 8) + response = apply_des(session_hash, keys).join + [cc.ljust(24, "\0"), response] + end + end + + + # base classes for primitives + # @private + class Field + attr_accessor :active, :value + + def initialize(opts) + @value = opts[:value] + @active = opts[:active].nil? ? true : opts[:active] + end + + def size + @active ? @size : 0 + end + end + + class String < Field + def initialize(opts) + super(opts) + @size = opts[:size] + end + + def parse(str, offset=0) + if @active and str.size >= offset + @size + @value = str[offset, @size] + @size + else + 0 + end + end + + def serialize + if @active + @value + else + "" + end + end + + def value=(val) + @value = val + @size = @value.nil? ? 0 : @value.size + @active = (@size > 0) + end + end + + class Int16LE < Field + def initialize(opt) + super(opt) + @size = 2 + end + def parse(str, offset=0) + if @active and str.size >= offset + @size + @value = str[offset, @size].unpack("v")[0] + @size + else + 0 + end + end + + def serialize + [@value].pack("v") + end + end + + class Int32LE < Field + def initialize(opt) + super(opt) + @size = 4 + end + + def parse(str, offset=0) + if @active and str.size >= offset + @size + @value = str.slice(offset, @size).unpack("V")[0] + @size + else + 0 + end + end + + def serialize + [@value].pack("V") if @active + end + end + + class Int64LE < Field + def initialize(opt) + super(opt) + @size = 8 + end + + def parse(str, offset=0) + if @active and str.size >= offset + @size + d, u = str.slice(offset, @size).unpack("V2") + @value = (u * 0x100000000 + d) + @size + else + 0 + end + end + + def serialize + [@value & 0x00000000ffffffff, @value >> 32].pack("V2") if @active + end + end + + # base class of data structure + class FieldSet + class << FieldSet + + + # @macro string_security_buffer + # @method $1 + # @method $1= + # @return [String] + def string(name, opts) + add_field(name, String, opts) + end + + # @macro int16le_security_buffer + # @method $1 + # @method $1= + # @return [Int16LE] + def int16LE(name, opts) + add_field(name, Int16LE, opts) + end + + # @macro int32le_security_buffer + # @method $1 + # @method $1= + # @return [Int32LE] + def int32LE(name, opts) + add_field(name, Int32LE, opts) + end + + # @macro int64le_security_buffer + # @method $1 + # @method $1= + # @return [Int64] + def int64LE(name, opts) + add_field(name, Int64LE, opts) + end + + # @macro security_buffer + # @method $1 + # @method $1= + # @return [SecurityBuffer] + def security_buffer(name, opts) + add_field(name, SecurityBuffer, opts) + end + + def prototypes + @proto + end + + def names + @proto.map{|n, t, o| n} + end + + def types + @proto.map{|n, t, o| t} + end + + def opts + @proto.map{|n, t, o| o} + end + + private + + def add_field(name, type, opts) + (@proto ||= []).push [name, type, opts] + define_accessor name + end + + def define_accessor(name) + module_eval(<<-End, __FILE__, __LINE__ + 1) + def #{name} + self['#{name}'].value + end + + def #{name}=(val) + self['#{name}'].value = val + end + End + end + end + + def initialize + @alist = self.class.prototypes.map{ |n, t, o| [n, t.new(o)] } + end + + def serialize + @alist.map{|n, f| f.serialize }.join + end + + def parse(str, offset=0) + @alist.inject(offset){|cur, a| cur += a[1].parse(str, cur)} + end + + def size + @alist.inject(0){|sum, a| sum += a[1].size} + end + + def [](name) + a = @alist.assoc(name.to_s.intern) + raise ArgumentError, "no such field: #{name}" unless a + a[1] + end + + def []=(name, val) + a = @alist.assoc(name.to_s.intern) + raise ArgumentError, "no such field: #{name}" unless a + a[1] = val + end + + def enable(name) + self[name].active = true + end + + def disable(name) + self[name].active = false + end + end + + class Blob < FieldSet + int32LE :blob_signature, {:value => BLOB_SIGN} + int32LE :reserved, {:value => 0} + int64LE :timestamp, {:value => 0} + string :challenge, {:value => "", :size => 8} + int32LE :unknown1, {:value => 0} + string :target_info, {:value => "", :size => 0} + int32LE :unknown2, {:value => 0} + end + + class SecurityBuffer < FieldSet + + int16LE :length, {:value => 0} + int16LE :allocated, {:value => 0} + int32LE :offset, {:value => 0} + + attr_accessor :active + def initialize(opts) + super() + @value = opts[:value] + @active = opts[:active].nil? ? true : opts[:active] + @size = 8 + end + + def parse(str, offset=0) + if @active and str.size >= offset + @size + super(str, offset) + @value = str[self.offset, self.length] + @size + else + 0 + end + end + + def serialize + super if @active + end + + def value + @value + end + + def value=(val) + @value = val + self.length = self.allocated = val.size + end + + def data_size + @active ? @value.size : 0 + end + end + + # @private false + class Message < FieldSet + class << Message + def parse(str) + m = Type0.new + m.parse(str) + case m.type + when 1 + t = Type1.parse(str) + when 2 + t = Type2.parse(str) + when 3 + t = Type3.parse(str) + else + raise ArgumentError, "unknown type: #{m.type}" + end + t + end + + def decode64(str) + parse(Base64.decode64(str)) + end + end + + def has_flag?(flag) + (self[:flag].value & FLAGS[flag]) == FLAGS[flag] + end + + def set_flag(flag) + self[:flag].value |= FLAGS[flag] + end + + def dump_flags + FLAG_KEYS.each{ |k| print(k, "=", flag?(k), "\n") } + end + + def serialize + deflag + super + security_buffers.map{|n, f| f.value}.join + end + + def encode64 + Base64.encode64(serialize).gsub(/\n/, '') + end + + def decode64(str) + parse(Base64.decode64(str)) + end + + alias head_size size + + def data_size + security_buffers.inject(0){|sum, a| sum += a[1].data_size} + end + + def size + head_size + data_size + end + + + def security_buffers + @alist.find_all{|n, f| f.instance_of?(SecurityBuffer)} + end + + def deflag + security_buffers.inject(head_size){|cur, a| + a[1].offset = cur + cur += a[1].data_size + } + end + + def data_edge + security_buffers.map{ |n, f| f.active ? f.offset : size}.min + end + + # sub class definitions + class Type0 < Message + string :sign, {:size => 8, :value => SSP_SIGN} + int32LE :type, {:value => 0} + end + + # @private false + class Type1 < Message + + string :sign, {:size => 8, :value => SSP_SIGN} + int32LE :type, {:value => 1} + int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE1] } + security_buffer :domain, {:value => ""} + security_buffer :workstation, {:value => Socket.gethostname } + string :padding, {:size => 0, :value => "", :active => false } + + class << Type1 + # Parses a Type 1 Message + # @param [String] str A string containing Type 1 data + # @return [Type1] The parsed Type 1 message + def parse(str) + t = new + t.parse(str) + t + end + end + + # @!visibility private + def parse(str) + super(str) + enable(:domain) if has_flag?(:DOMAIN_SUPPLIED) + enable(:workstation) if has_flag?(:WORKSTATION_SUPPLIED) + super(str) + if ( (len = data_edge - head_size) > 0) + self.padding = "\0" * len + super(str) + end + end + end + + + # @private false + class Type2 < Message + + string :sign, {:size => 8, :value => SSP_SIGN} + int32LE :type, {:value => 2} + security_buffer :target_name, {:size => 0, :value => ""} + int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE2]} + int64LE :challenge, {:value => 0} + int64LE :context, {:value => 0, :active => false} + security_buffer :target_info, {:value => "", :active => false} + string :padding, {:size => 0, :value => "", :active => false } + + class << Type2 + # Parse a Type 2 packet + # @param [String] str A string containing Type 2 data + # @return [Type2] + def parse(str) + t = new + t.parse(str) + t + end + end + + # @!visibility private + def parse(str) + super(str) + if has_flag?(:TARGET_INFO) + enable(:context) + enable(:target_info) + super(str) + end + if ( (len = data_edge - head_size) > 0) + self.padding = "\0" * len + super(str) + end + end + + # Generates a Type 3 response based on the Type 2 Information + # @return [Type3] + # @option arg [String] :username The username to authenticate with + # @option arg [String] :password The user's password + # @option arg [String] :domain ('') The domain to authenticate to + # @option opt [String] :workstation (Socket.gethostname) The name of the calling workstation + # @option opt [Boolean] :use_default_target (False) Use the domain supplied by the server in the Type 2 packet + # @note An empty :domain option authenticates to the local machine. + # @note The :use_default_target has presidence over the :domain option + def response(arg, opt = {}) + usr = arg[:user] + pwd = arg[:password] + domain = arg[:domain] ? arg[:domain] : "" + if usr.nil? or pwd.nil? + raise ArgumentError, "user and password have to be supplied" + end + + if opt[:workstation] + ws = opt[:workstation] + else + ws = Socket.gethostname + end + + if opt[:client_challenge] + cc = opt[:client_challenge] + else + cc = rand(MAX64) + end + cc = NTLM::pack_int64le(cc) if cc.is_a?(Integer) + opt[:client_challenge] = cc + + if has_flag?(:OEM) and opt[:unicode] + usr = NTLM::EncodeUtil.decode_utf16le(usr) + pwd = NTLM::EncodeUtil.decode_utf16le(pwd) + ws = NTLM::EncodeUtil.decode_utf16le(ws) + domain = NTLM::EncodeUtil.decode_utf16le(domain) + opt[:unicode] = false + end + + if has_flag?(:UNICODE) and !opt[:unicode] + usr = NTLM::EncodeUtil.encode_utf16le(usr) + pwd = NTLM::EncodeUtil.encode_utf16le(pwd) + ws = NTLM::EncodeUtil.encode_utf16le(ws) + domain = NTLM::EncodeUtil.encode_utf16le(domain) + opt[:unicode] = true + end + + if opt[:use_default_target] + domain = self.target_name + end + + ti = self.target_info + + chal = self[:challenge].serialize + + if opt[:ntlmv2] + ar = {:ntlmv2_hash => NTLM::ntlmv2_hash(usr, pwd, domain, opt), :challenge => chal, :target_info => ti} + lm_res = NTLM::lmv2_response(ar, opt) + ntlm_res = NTLM::ntlmv2_response(ar, opt) + elsif has_flag?(:NTLM2_KEY) + ar = {:ntlm_hash => NTLM::ntlm_hash(pwd, opt), :challenge => chal} + lm_res, ntlm_res = NTLM::ntlm2_session(ar, opt) + else + lm_res = NTLM::lm_response(pwd, chal) + ntlm_res = NTLM::ntlm_response(pwd, chal) + end + + Type3.create({ + :lm_response => lm_res, + :ntlm_response => ntlm_res, + :domain => domain, + :user => usr, + :workstation => ws, + :flag => self.flag + }) + end + end + + # @private false + class Type3 < Message + + string :sign, {:size => 8, :value => SSP_SIGN} + int32LE :type, {:value => 3} + security_buffer :lm_response, {:value => ""} + security_buffer :ntlm_response, {:value => ""} + security_buffer :domain, {:value => ""} + security_buffer :user, {:value => ""} + security_buffer :workstation, {:value => ""} + security_buffer :session_key, {:value => "", :active => false } + int64LE :flag, {:value => 0, :active => false } + + class << Type3 + # Parse a Type 3 packet + # @param [String] str A string containing Type 3 data + # @return [Type2] + def parse(str) + t = new + t.parse(str) + t + end + + # Builds a Type 3 packet + # @note All options must be properly encoded with either unicode or oem encoding + # @return [Type3] + # @option arg [String] :lm_response The LM hash + # @option arg [String] :ntlm_response The NTLM hash + # @option arg [String] :domain The domain to authenticate to + # @option arg [String] :workstation The name of the calling workstation + # @option arg [String] :session_key The session key + # @option arg [Integer] :flag Flags for the packet + def create(arg, opt ={}) + t = new + t.lm_response = arg[:lm_response] + t.ntlm_response = arg[:ntlm_response] + t.domain = arg[:domain] + t.user = arg[:user] + + if arg[:workstation] + t.workstation = arg[:workstation] + end + + if arg[:session_key] + t.enable(:session_key) + t.session_key = arg[session_key] + end + + if arg[:flag] + t.enable(:session_key) + t.enable(:flag) + t.flag = arg[:flag] + end + t + end + end + end + end + end +end