321 lines
9.2 KiB
Ruby
321 lines
9.2 KiB
Ruby
|
#
|
||
|
# 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.
|
||
|
#
|
||
|
# 2011-02-23 refactored and improved by Alexandre Maloteaux for Metasploit Project
|
||
|
# -------------------------------------------------------------
|
||
|
#
|
||
|
# 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 11678 2011-01-30 19:26:35Z hdm $
|
||
|
#++
|
||
|
|
||
|
|
||
|
require 'rex/proto/ntlm/constants'
|
||
|
require 'rex/proto/ntlm/base'
|
||
|
|
||
|
module Rex
|
||
|
module Proto
|
||
|
module NTLM
|
||
|
class Crypt
|
||
|
|
||
|
CONST = Rex::Proto::NTLM::Constants
|
||
|
BASE = Rex::Proto::NTLM::Base
|
||
|
|
||
|
@@loaded_openssl = false
|
||
|
|
||
|
begin
|
||
|
require 'openssl'
|
||
|
require 'openssl/digest'
|
||
|
@@loaded_openssl = true
|
||
|
rescue ::Exception
|
||
|
end
|
||
|
|
||
|
begin
|
||
|
|
||
|
def self.gen_keys(str)
|
||
|
Rex::Text::split_to_a(str, 7).map{ |str7|
|
||
|
bits = Rex::Text::split_to_a(str7.unpack("B*")[0], 7).inject('')\
|
||
|
{|ret, tkn| ret += tkn + (tkn.gsub('1', '').size % 2).to_s }
|
||
|
[bits].pack("B*")
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def self.apply_des(plain, keys)
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
dec = OpenSSL::Cipher::DES.new
|
||
|
keys.map {|k|
|
||
|
dec.key = k
|
||
|
dec.encrypt.update(plain)
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def self.lm_hash(password, half = false)
|
||
|
if half then size = 7 else size = 14 end
|
||
|
keys = gen_keys password.upcase.ljust(size, "\0")
|
||
|
apply_des(CONST::LM_MAGIC, keys).join
|
||
|
end
|
||
|
|
||
|
def self.ntlm_hash(password, opt = {})
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
pwd = password.dup
|
||
|
unless opt[:unicode]
|
||
|
pwd = Rex::Text.to_unicode(pwd)
|
||
|
end
|
||
|
OpenSSL::Digest::MD4.digest pwd
|
||
|
end
|
||
|
|
||
|
def self.ntlmv2_hash(user, password, domain, opt={})
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
ntlmhash = ntlm_hash(password, opt)
|
||
|
#With Win 7 and maybe other OSs we sometimes get my domain not uppercased,
|
||
|
#so the domain does not need to be uppercased
|
||
|
userdomain = user.upcase + domain
|
||
|
unless opt[:unicode]
|
||
|
userdomain = Rex::Text.to_unicode(userdomain)
|
||
|
end
|
||
|
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain)
|
||
|
end
|
||
|
|
||
|
# responses
|
||
|
def self.lm_response(arg, half = false)
|
||
|
begin
|
||
|
hash = arg[:lm_hash]
|
||
|
chal = arg[:challenge]
|
||
|
rescue
|
||
|
raise ArgumentError
|
||
|
end
|
||
|
chal = BASE::pack_int64le(chal) if chal.is_a?(Integer)
|
||
|
if half then size = 7 else size = 21 end
|
||
|
keys = gen_keys hash.ljust(size, "\0")
|
||
|
apply_des(chal, keys).join
|
||
|
end
|
||
|
|
||
|
#synonym of lm_response for old compatibility with lib/rex/proto/smb/crypt
|
||
|
def self.lanman_des(password, challenge)
|
||
|
arglm = { :lm_hash => self.lm_hash(password),
|
||
|
:challenge => challenge }
|
||
|
self.lm_response(arglm)
|
||
|
end
|
||
|
|
||
|
def self.ntlm_response(arg)
|
||
|
hash = arg[:ntlm_hash]
|
||
|
chal = arg[:challenge]
|
||
|
chal = BASE::pack_int64le(chal) if chal.is_a?(::Integer)
|
||
|
keys = gen_keys hash.ljust(21, "\0")
|
||
|
apply_des(chal, keys).join
|
||
|
end
|
||
|
|
||
|
#synonym of ntlm_response for old compatibility with lib/rex/proto/smb/crypt
|
||
|
def self.ntlm_md4(password, challenge)
|
||
|
argntlm = { :ntlm_hash => self.ntlm_hash(password),
|
||
|
:challenge => challenge }
|
||
|
self.ntlm_response(argntlm)
|
||
|
end
|
||
|
|
||
|
def self.ntlmv2_response(arg, opt = {})
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
begin
|
||
|
key = arg[:ntlmv2_hash]
|
||
|
chal = arg[:challenge]
|
||
|
rescue
|
||
|
raise ArgumentError , 'ntlmv2_hash and challenge are mandatory'
|
||
|
end
|
||
|
chal = BASE::pack_int64le(chal) if chal.is_a?(::Integer)
|
||
|
if opt[:nt_client_challenge]
|
||
|
unless opt[:nt_client_challenge].is_a?(::String) && opt[:nt_client_challenge].length > 24
|
||
|
raise ArgumentError,"nt_client_challenge is not in a correct format "
|
||
|
end
|
||
|
bb = opt[:nt_client_challenge]
|
||
|
else
|
||
|
begin
|
||
|
ti = arg[:target_info]
|
||
|
rescue
|
||
|
raise ArgumentError, "target_info is mandatory in this case"
|
||
|
end
|
||
|
if opt[:client_challenge]
|
||
|
cc = opt[:client_challenge]
|
||
|
else
|
||
|
cc = rand(CONST::MAX64)
|
||
|
end
|
||
|
cc = BASE::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 + CONST::TIME_OFFSET)
|
||
|
|
||
|
blob = BASE::Blob.new
|
||
|
blob.timestamp = ts
|
||
|
blob.challenge = cc
|
||
|
blob.target_info = ti
|
||
|
|
||
|
bb = blob.serialize
|
||
|
end
|
||
|
|
||
|
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + bb) + bb
|
||
|
|
||
|
end
|
||
|
|
||
|
|
||
|
def self.lmv2_response(arg, opt = {})
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
key = arg[:ntlmv2_hash]
|
||
|
chal = arg[:challenge]
|
||
|
|
||
|
chal = BASE::pack_int64le(chal) if chal.is_a?(::Integer)
|
||
|
if opt[:client_challenge]
|
||
|
cc = opt[:client_challenge]
|
||
|
else
|
||
|
cc = rand(CONST::MAX64)
|
||
|
end
|
||
|
cc = BASE::pack_int64le(cc) if cc.is_a?(::Integer)
|
||
|
|
||
|
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, key, chal + cc) + cc
|
||
|
end
|
||
|
|
||
|
def self.ntlm2_session(arg, opt = {})
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
begin
|
||
|
passwd_hash = arg[:ntlm_hash]
|
||
|
chal = arg[:challenge]
|
||
|
rescue
|
||
|
raise ArgumentError
|
||
|
end
|
||
|
|
||
|
if opt[:client_challenge]
|
||
|
cc = opt[:client_challenge]
|
||
|
else
|
||
|
cc = rand(CONST::MAX64)
|
||
|
end
|
||
|
cc = BASE::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
|
||
|
|
||
|
#signing method added for metasploit project
|
||
|
|
||
|
#Used when only the LMv1 response is provided (i.e., with Win9x clients)
|
||
|
def self.lmv1_user_session_key(pass )
|
||
|
self.lm_hash(pass.upcase[0,7],true).ljust(16,"\x00")
|
||
|
end
|
||
|
|
||
|
#This variant is used when the client sends the NTLMv1 response
|
||
|
def self.ntlmv1_user_session_key(pass )
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
OpenSSL::Digest::MD4.digest(self.ntlm_hash(pass))
|
||
|
end
|
||
|
|
||
|
#Used when NTLMv1 authentication is employed with NTLM2 session security
|
||
|
def self.ntlm2_session_user_session_key(pass, srv_chall, cli_chall)
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
ntlm_key = self.ntlmv1_user_session_key(pass )
|
||
|
session_chal = srv_chall + cli_chall
|
||
|
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlm_key, session_chal)
|
||
|
end
|
||
|
|
||
|
#Used when the LMv2 response is sent
|
||
|
def self.lmv2_user_session_key(user, pass, domain, srv_chall, cli_chall)
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
ntlmv2_key = self.ntlmv2_hash(user, pass, domain)
|
||
|
hash1 = OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_key, srv_chall + cli_chall)
|
||
|
OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmv2_key, hash1)
|
||
|
end
|
||
|
|
||
|
#Used when the NTLMv2 response is sent
|
||
|
class << self; alias_method :ntlmv2_user_session_key, :lmv2_user_session_key; end
|
||
|
|
||
|
#Used when LAnMan Key flag is set
|
||
|
def self.lanman_session_key(pass, srvchall)
|
||
|
halfhash =self.lm_hash(pass.upcase[0,7],true)
|
||
|
arglm = { :lm_hash => halfhash[0,7],
|
||
|
:challenge => srvchall }
|
||
|
plain = self.lm_response(arglm,true)
|
||
|
key = halfhash + ["bdbdbdbdbdbd"].pack("H*")
|
||
|
keys = self.gen_keys(key)
|
||
|
self.apply_des(plain, keys).join
|
||
|
end
|
||
|
|
||
|
|
||
|
def self.encrypt_sessionkey(session_key, user_session_key)
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
cipher = OpenSSL::Cipher::Cipher.new('rc4')
|
||
|
cipher.encrypt
|
||
|
cipher.key = user_session_key
|
||
|
cipher.update(session_key)
|
||
|
end
|
||
|
|
||
|
def self.decrypt_sessionkey(encrypted_session_key, user_session_key)
|
||
|
raise RuntimeError, "No OpenSSL support" if not @@loaded_openssl
|
||
|
cipher = OpenSSL::Cipher::Cipher.new('rc4')
|
||
|
cipher.decrypt
|
||
|
cipher.key = user_session_key
|
||
|
cipher.update(encrypted_session_key)
|
||
|
end
|
||
|
|
||
|
def self.make_weak_sessionkey(session_key,key_size,lanman_key = false)
|
||
|
case key_size
|
||
|
when 40
|
||
|
if lanman_key
|
||
|
return session_key[0,5] + "\xe5\x38\xb0"
|
||
|
else
|
||
|
return session_key[0,5]
|
||
|
end
|
||
|
when 56
|
||
|
if lanman_key
|
||
|
return session_key[0,7] + "\xa0"
|
||
|
else
|
||
|
return session_key[0,7]
|
||
|
end
|
||
|
else #128
|
||
|
return session_key[0,16]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
rescue LoadError
|
||
|
end
|
||
|
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|