diff --git a/lib/net/ntlm.rb b/lib/net/ntlm.rb new file mode 100755 index 0000000000..05d0579079 --- /dev/null +++ b/lib/net/ntlm.rb @@ -0,0 +1,775 @@ +# +# = 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$ +#++ + +require 'base64' +require 'openssl' +require 'openssl/digest' +require 'kconv' + +module Net #:nodoc: + module NTLM + + module VERSION #:nodoc: + MAJOR = 0 + MINOR = 1 + 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, + # :UNKNOWN => 0x00000008, + :SIGN => 0x00000010, + :SEAL => 0x00000020, + # :UNKNOWN => 0x00000040, + :NETWARE => 0x00000100, + :NTLM => 0x00000200, + # :UNKNOWN => 0x00000400, + # :UNKNOWN => 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 + } + + 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] + } + + # module functions + class << self + def decode_utf16le(str) + Kconv.kconv(swap16(str), Kconv::ASCII, Kconv::UTF16) + end + + def encode_utf16le(str) + swap16(Kconv.kconv(str, Kconv::UTF16, Kconv::ASCII)) + end + + def pack_int64le(val) + [val & 0x00000000ffffffff, val >> 32].pack("V2") + end + + def swap16(str) + str.unpack("v*").pack("n*") + end + + def split7(str) + s = str.dup + until s.empty? + (ret ||= []).push s.slice!(0, 7) + end + ret + end + + 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 + + def lm_hash(password) + keys = gen_keys password.upcase.ljust(14, "\0") + apply_des(LM_MAGIC, keys).join + end + + def ntlm_hash(password, opt = {}) + pwd = password.dup + unless opt[:unicode] + pwd = encode_utf16le(pwd) + end + OpenSSL::Digest::MD4.digest pwd + end + + 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 + + # responses + 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 + 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 + def define(&block) + c = Class.new(self) + def c.inherited(subclass) + proto = @proto + subclass.instance_eval { + @proto = proto + } + end + c.module_eval(&block) + c + end + + def string(name, opts) + add_field(name, String, opts) + end + + def int16LE(name, opts) + add_field(name, Int16LE, opts) + end + + def int32LE(name, opts) + add_field(name, Int32LE, opts) + end + + def int64LE(name, opts) + add_field(name, Int64LE, opts) + end + + 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 + + + Blob = FieldSet.define { + 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} + } + + SecurityBuffer = FieldSet.define { + int16LE :length, {:value => 0} + int16LE :allocated, {:value => 0} + int32LE :offset, {:value => 0} + } + + class SecurityBuffer + 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 + + 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 + + + private + + 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 + + Type0 = Message.define { + string :sign, {:size => 8, :value => SSP_SIGN} + int32LE :type, {:value => 0} + } + + Type1 = Message.define { + string :sign, {:size => 8, :value => SSP_SIGN} + int32LE :type, {:value => 1} + int32LE :flag, {:value => DEFAULT_FLAGS[:TYPE1] } + security_buffer :domain, {:value => "", :active => false} + security_buffer :workstation, {:value => "", :active => false} + string :padding, {:size => 0, :value => "", :active => false } + } + + class Type1 + class << Type1 + def parse(str) + t = new + t.parse(str) + t + end + end + + 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 + + Type2 = Message.define{ + 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 + class << Type2 + def parse(str) + t = new + t.parse(str) + t + end + end + + 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 + + def response(arg, opt = {}) + usr = arg[:user] + pwd = arg[:password] + if usr.nil? or pwd.nil? + raise ArgumentError, "user and password have to be supplied" + end + + if opt[:workstation] + ws = opt[:workstation] + else + ws = "" + 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) + 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) + opt[:unicode] = true + end + + tgt = self.target_name + ti = self.target_info + + chal = self[:challenge].serialize + + if opt[:ntlmv2] + ar = {:ntlmv2_hash => NTLM::ntlmv2_hash(usr, pwd, tgt, 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 => tgt, + :user => usr, + :workstation => ws, + :flag => self.flag + }) + end + end + + + Type3 = Message.define{ + 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 + class << Type3 + def parse(str) + t = new + t.parse(str) + t + end + + 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] + t.workstation = arg[:workstation] + + 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 diff --git a/lib/net/ntlm.rb.ut.rb b/lib/net/ntlm.rb.ut.rb new file mode 100755 index 0000000000..b37e998ff2 --- /dev/null +++ b/lib/net/ntlm.rb.ut.rb @@ -0,0 +1,174 @@ +#!/usr/bin/env ruby +require 'test/unit' +require 'net/ntlm' +require 'rex/socket' + +class ConnectionTest < Test::Unit::TestCase + def setup + @user = "admin" + @pass = "1234" + @domain = "" + @host = "192.168.145.161" + end + + def test_socket_connectivity + assert_nothing_raised do + socket = Rex::Socket.create_tcp( + 'PeerHost' => @host, + 'PeerPort' => 80 + ) + assert_kind_of Socket, socket + assert !socket.closed? + socket.close + assert socket.closed? + end + end + + def http_message(msg) + get_req = "GET / HTTP/1.1\r\n" + get_req += "Host: #{@host}\r\n" + get_req += "Authorization: NTLM #{msg.encode64}\r\n" + get_req += "\r\n" + end + + def client_auth(pw) + msg_1 = Net::NTLM::Message::Type1.new + get_req = http_message(msg_1) + socket = Rex::Socket.create_tcp( + 'PeerHost' => @host, + 'PeerPort' => 80 + ) + socket.put get_req + res = socket.get(3) + assert res =~ /WWW-Authenticate: NTLM TlRM/ + res_ntlm = res.match(/WWW-Authenticate: NTLM ([A-Z0-9\x2b\x2f=]+)/i)[1] + assert_operator res_ntlm.size, :>=, 24 + msg_2 = Net::NTLM::Message.decode64(res_ntlm) + assert msg_2 + msg_3 = msg_2.response({:user => @user, :password => pw}, {:ntlmv2 => true}) + assert msg_3 + auth_req = http_message(msg_3) + socket.put auth_req + auth_res = socket.get(3) + socket.close + return auth_res + end + + def test_client_auth_success + assert_equal client_auth(@pass)[0,12], "HTTP/1.1 200" + end + + def test_client_auth_fail + assert_not_equal client_auth("badpass")[0,12], "HTTP/1.1 200" + assert_equal client_auth("badpass")[0,12], "HTTP/1.1 401" + end +end + +# FunctionTest by Minero Aoki + +class FunctionTest < Test::Unit::TestCase #:nodoc: + def setup + @passwd = "SecREt01" + @user = "user" + @domain = "domain" + @challenge = ["0123456789abcdef"].pack("H*") + @client_ch = ["ffffff0011223344"].pack("H*") + @timestamp = 1055844000 + @trgt_info = [ + "02000c0044004f004d00410049004e00" + + "01000c00530045005200560045005200" + + "0400140064006f006d00610069006e00" + + "2e0063006f006d000300220073006500" + + "72007600650072002e0064006f006d00" + + "610069006e002e0063006f006d000000" + + "0000" + ].pack("H*") + end + + def test_lm_hash + ahash = ["ff3750bcc2b22412c2265b23734e0dac"].pack("H*") + assert_equal ahash, Net::NTLM::lm_hash(@passwd) + end + + def test_ntlm_hash + ahash = ["cd06ca7c7e10c99b1d33b7485a2ed808"].pack("H*") + assert_equal ahash, Net::NTLM::ntlm_hash(@passwd) + end + + def test_ntlmv2_hash + ahash = ["04b8e0ba74289cc540826bab1dee63ae"].pack("H*") + assert_equal ahash, Net::NTLM::ntlmv2_hash(@user, @passwd, @domain) + end + + def test_lm_response + ares = ["c337cd5cbd44fc9782a667af6d427c6de67c20c2d3e77c56"].pack("H*") + assert_equal ares, Net::NTLM::lm_response( + { + :lm_hash => Net::NTLM::lm_hash(@passwd), + :challenge => @challenge + } + ) + end + + def test_ntlm_response + ares = ["25a98c1c31e81847466b29b2df4680f39958fb8c213a9cc6"].pack("H*") + ntlm_hash = Net::NTLM::ntlm_hash(@passwd) + assert_equal ares, Net::NTLM::ntlm_response( + { + :ntlm_hash => ntlm_hash, + :challenge => @challenge + } + ) + end + + def test_lmv2_response + ares = ["d6e6152ea25d03b7c6ba6629c2d6aaf0ffffff0011223344"].pack("H*") + assert_equal ares, Net::NTLM::lmv2_response( + { + :ntlmv2_hash => Net::NTLM::ntlmv2_hash(@user, @passwd, @domain), + :challenge => @challenge + }, + { :client_challenge => @client_ch } + ) + end + + def test_ntlmv2_response + ares = [ + "cbabbca713eb795d04c97abc01ee4983" + + "01010000000000000090d336b734c301" + + "ffffff00112233440000000002000c00" + + "44004f004d00410049004e0001000c00" + + "53004500520056004500520004001400" + + "64006f006d00610069006e002e006300" + + "6f006d00030022007300650072007600" + + "650072002e0064006f006d0061006900" + + "6e002e0063006f006d00000000000000" + + "0000" + ].pack("H*") + assert_equal ares, Net::NTLM::ntlmv2_response( + { + :ntlmv2_hash => Net::NTLM::ntlmv2_hash(@user, @passwd, @domain), + :challenge => @challenge, + :target_info => @trgt_info + }, + { + :timestamp => @timestamp, + :client_challenge => @client_ch + } + ) + end + + def test_ntlm2_session + acha = ["ffffff001122334400000000000000000000000000000000"].pack("H*") + ares = ["10d550832d12b2ccb79d5ad1f4eed3df82aca4c3681dd455"].pack("H*") + session = Net::NTLM::ntlm2_session( + { + :ntlm_hash => Net::NTLM::ntlm_hash(@passwd), + :challenge => @challenge + }, + { :client_challenge => @client_ch } + ) + assert_equal acha, session[0] + assert_equal ares, session[1] + end +end