563 lines
15 KiB
Ruby
563 lines
15 KiB
Ruby
require 'rex/text'
|
|
require 'rex/proto/smb/constants'
|
|
|
|
module Rex
|
|
module Proto
|
|
module SMB
|
|
class Utils
|
|
|
|
CONST = Rex::Proto::SMB::Constants
|
|
|
|
# Creates an access mask for use with the CLIENT.open() call based on a string
|
|
def self.open_mode_to_access(str)
|
|
access = CONST::OPEN_ACCESS_READ | CONST::OPEN_SHARE_DENY_NONE
|
|
str.each_byte { |c|
|
|
case [c].pack('C').downcase
|
|
when 'w'
|
|
access |= CONST::OPEN_ACCESS_READWRITE
|
|
end
|
|
}
|
|
return access
|
|
end
|
|
|
|
# Creates a mode mask for use with the CLIENT.open() call based on a string
|
|
def self.open_mode_to_mode(str)
|
|
mode = 0
|
|
|
|
str.each_byte { |c|
|
|
case [c].pack('C').downcase
|
|
when 'x' # Fail if the file already exists
|
|
mode |= CONST::OPEN_MODE_EXCL
|
|
when 't' # Truncate the file if it already exists
|
|
mode |= CONST::OPEN_MODE_TRUNC
|
|
when 'c' # Create the file if it does not exist
|
|
mode |= CONST::OPEN_MODE_CREAT
|
|
when 'o' # Just open the file, clashes with x
|
|
mode |= CONST::OPEN_MODE_OPEN
|
|
end
|
|
}
|
|
|
|
return mode
|
|
end
|
|
|
|
# Returns a disposition value for smb.create based on permission string
|
|
def self.create_mode_to_disposition(str)
|
|
str.each_byte { |c|
|
|
case [c].pack('C').downcase
|
|
when 'c' # Create the file if it does not exist
|
|
return CONST::CREATE_ACCESS_OPENCREATE
|
|
when 'o' # Just open the file and fail if it does not exist
|
|
return CONST::CREATE_ACCESS_EXIST
|
|
end
|
|
}
|
|
|
|
return CONST::CREATE_ACCESS_OPENCREATE
|
|
end
|
|
|
|
# Convert a 64-bit signed SMB time to a unix timestamp
|
|
def self.time_smb_to_unix(thi, tlo)
|
|
(((thi << 32) + tlo) / 10000000) - 11644473600
|
|
end
|
|
|
|
# Convert a unix timestamp to a 64-bit signed server time
|
|
def self.time_unix_to_smb(unix_time)
|
|
t64 = (unix_time + 11644473600) * 10000000
|
|
thi = (t64 & 0xffffffff00000000) >> 32
|
|
tlo = (t64 & 0x00000000ffffffff)
|
|
return [thi, tlo]
|
|
end
|
|
|
|
# Convert a name to its NetBIOS equivalent
|
|
def self.nbname_encode(str)
|
|
encoded = ''
|
|
for x in (0..15)
|
|
if (x >= str.length)
|
|
encoded << 'CA'
|
|
else
|
|
c = str[x, 1].upcase[0,1].unpack('C*')[0]
|
|
encoded << [ (c / 16) + 0x41, (c % 16) + 0x41 ].pack('CC')
|
|
end
|
|
end
|
|
return encoded
|
|
end
|
|
|
|
# Convert a name from its NetBIOS equivalent
|
|
def self.nbname_decode(str)
|
|
decoded = ''
|
|
str << 'A' if str.length % 2 != 0
|
|
while (str.length > 0)
|
|
two = str.slice!(0, 2).unpack('C*')
|
|
if (two.length == 2)
|
|
decoded << [ ((two[0] - 0x41) * 16) + two[1] - 0x41 ].pack('C')
|
|
end
|
|
end
|
|
return decoded
|
|
end
|
|
|
|
#
|
|
# Prepends an ASN1 formatted length field to a piece of data
|
|
#
|
|
def self.asn1encode(str = '')
|
|
res = ''
|
|
|
|
# If the high bit of the first byte is 1, it contains the number of
|
|
# length bytes that follow
|
|
|
|
case str.length
|
|
when 0 .. 0x7F
|
|
res = [str.length].pack('C') + str
|
|
when 0x80 .. 0xFF
|
|
res = [0x81, str.length].pack('CC') + str
|
|
when 0x100 .. 0xFFFF
|
|
res = [0x82, str.length].pack('Cn') + str
|
|
when 0x10000 .. 0xffffff
|
|
res = [0x83, str.length >> 16, str.length & 0xFFFF].pack('CCn') + str
|
|
when 0x1000000 .. 0xffffffff
|
|
res = [0x84, str.length].pack('CN') + str
|
|
else
|
|
raise "ASN1 str too long"
|
|
end
|
|
return res
|
|
end
|
|
|
|
def self.make_ntlmv2_secblob_init(domain = 'WORKGROUP', name = 'WORKSTATION', flags=0x80201)
|
|
blob =
|
|
"\x60" + self.asn1encode(
|
|
"\x06" + self.asn1encode(
|
|
"\x2b\x06\x01\x05\x05\x02"
|
|
) +
|
|
"\xa0" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\xa0" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\x06" + self.asn1encode(
|
|
"\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a"
|
|
)
|
|
)
|
|
) +
|
|
"\xa2" + self.asn1encode(
|
|
"\x04" + self.asn1encode(
|
|
"NTLMSSP\x00" +
|
|
[1, flags].pack('VV') +
|
|
|
|
[
|
|
domain.length, #length
|
|
domain.length, #max length
|
|
32
|
|
].pack('vvV') +
|
|
|
|
[
|
|
name.length, #length
|
|
name.length, #max length
|
|
domain.length + 32
|
|
].pack('vvV') +
|
|
|
|
domain + name
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
return blob
|
|
end
|
|
|
|
def self.make_ntlmv2_secblob_auth(domain, name, user, lmv2, ntlm, flags = 0x080201)
|
|
|
|
lmv2 ||= "\x00" * 24
|
|
ntlm ||= "\x00" * 24
|
|
|
|
domain_uni = Rex::Text.to_unicode(domain)
|
|
user_uni = Rex::Text.to_unicode(user)
|
|
name_uni = Rex::Text.to_unicode(name)
|
|
session = ''
|
|
|
|
ptr = 64
|
|
blob =
|
|
"\xa1" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\xa2" + self.asn1encode(
|
|
"\x04" + self.asn1encode(
|
|
|
|
"NTLMSSP\x00" +
|
|
[ 3 ].pack('V') +
|
|
|
|
[ # Lan Manager Response
|
|
lmv2.length,
|
|
lmv2.length,
|
|
(ptr)
|
|
].pack('vvV') +
|
|
|
|
[ # NTLM Manager Response
|
|
ntlm.length,
|
|
ntlm.length,
|
|
(ptr += lmv2.length)
|
|
].pack('vvV') +
|
|
|
|
[ # Domain Name
|
|
domain_uni.length,
|
|
domain_uni.length,
|
|
(ptr += ntlm.length)
|
|
].pack('vvV') +
|
|
|
|
[ # Username
|
|
user_uni.length,
|
|
user_uni.length,
|
|
(ptr += domain_uni.length)
|
|
].pack('vvV') +
|
|
|
|
[ # Hostname
|
|
name_uni.length,
|
|
name_uni.length,
|
|
(ptr += user_uni.length)
|
|
].pack('vvV') +
|
|
|
|
[ # Session Key (none)
|
|
session.length,
|
|
session.length,
|
|
(ptr += name_uni.length)
|
|
].pack('vvV') +
|
|
|
|
[ flags ].pack('V') +
|
|
|
|
lmv2 +
|
|
ntlm +
|
|
domain_uni +
|
|
user_uni +
|
|
name_uni +
|
|
session + "\x00"
|
|
)
|
|
)
|
|
)
|
|
)
|
|
return blob
|
|
end
|
|
|
|
|
|
def self.make_negotiate_secblob_resp(account, domain)
|
|
blob =
|
|
"\x60" + self.asn1encode(
|
|
"\x06" + self.asn1encode(
|
|
"\x2b\x06\x01\x05\x05\x02"
|
|
) +
|
|
"\xa0" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\xa0" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\x06" + self.asn1encode(
|
|
"\x2a\x86\x48\x82\xf7\x12\x01\x02\x02"
|
|
) +
|
|
"\x06" + self.asn1encode(
|
|
"\x2a\x86\x48\x86\xf7\x12\x01\x02\x02"
|
|
) +
|
|
"\x06" + self.asn1encode(
|
|
"\x2a\x86\x48\x86\xf7\x12\x01\x02\x02\x03"
|
|
) +
|
|
"\x06" + self.asn1encode(
|
|
"\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a"
|
|
)
|
|
)
|
|
) +
|
|
"\xa3" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\xa0" + self.asn1encode(
|
|
"\x1b" + self.asn1encode(
|
|
account + '@' + domain
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
return blob
|
|
end
|
|
|
|
def self.make_ntlmv2_secblob_chall(win_domain, dns_domain, win_name, dns_name, chall, flags)
|
|
|
|
win_domain = Rex::Text.to_unicode(win_domain)
|
|
dns_domain = Rex::Text.to_unicode(dns_domain)
|
|
win_name = Rex::Text.to_unicode(win_name)
|
|
dns_name = Rex::Text.to_unicode(dns_name)
|
|
|
|
addr_list = ''
|
|
addr_list << [2, win_domain.length].pack('vv') + win_domain
|
|
addr_list << [1, win_name.length].pack('vv') + win_name
|
|
addr_list << [4, dns_domain.length].pack('vv') + dns_domain
|
|
addr_list << [3, dns_name.length].pack('vv') + dns_name
|
|
addr_list << [5, dns_domain.length].pack('vv') + dns_domain
|
|
addr_list << [0, 0].pack('vv')
|
|
|
|
ptr = 0
|
|
blob =
|
|
"\xa1" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\xa0" + self.asn1encode(
|
|
"\x0a" + self.asn1encode(
|
|
"\x01"
|
|
)
|
|
) +
|
|
"\xa1" + self.asn1encode(
|
|
"\x06" + self.asn1encode(
|
|
"\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a"
|
|
)
|
|
) +
|
|
"\xa2" + self.asn1encode(
|
|
"\x04" + self.asn1encode(
|
|
"NTLMSSP\x00" +
|
|
[2].pack('V') +
|
|
[
|
|
win_domain.length, # length
|
|
win_domain.length, # max length
|
|
(ptr += 48)
|
|
].pack('vvV') +
|
|
[ flags ].pack('V') +
|
|
chall +
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00" +
|
|
[
|
|
addr_list.length, # length
|
|
addr_list.length, # max length
|
|
(ptr += win_domain.length)
|
|
].pack('vvV') +
|
|
win_domain +
|
|
addr_list
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
return blob
|
|
end
|
|
|
|
def self.make_ntlmv2_secblob_success
|
|
blob =
|
|
"\xa1" + self.asn1encode(
|
|
"\x30" + self.asn1encode(
|
|
"\xa0" + self.asn1encode(
|
|
"\x0a" + self.asn1encode(
|
|
"\x00"
|
|
)
|
|
)
|
|
)
|
|
)
|
|
return blob
|
|
end
|
|
|
|
#
|
|
# Process Type 3 NTLM Message (in Base64)
|
|
#
|
|
# from http://www.innovation.ch/personal/ronald/ntlm.html
|
|
#
|
|
# struct {
|
|
# byte protocol[8]; // 'N', 'T', 'L', 'M', 'S', 'S', 'P', '\0'
|
|
# byte type; // 0x03
|
|
# byte zero[3];
|
|
#
|
|
# short lm_resp_len; // LanManager response length (always 0x18)
|
|
# short lm_resp_len; // LanManager response length (always 0x18)
|
|
# short lm_resp_off; // LanManager response offset
|
|
# byte zero[2];
|
|
#
|
|
# short nt_resp_len; // NT response length (always 0x18)
|
|
# short nt_resp_len; // NT response length (always 0x18)
|
|
# short nt_resp_off; // NT response offset
|
|
# byte zero[2];
|
|
#
|
|
# short dom_len; // domain string length
|
|
# short dom_len; // domain string length
|
|
# short dom_off; // domain string offset (always 0x40)
|
|
# byte zero[2];
|
|
#
|
|
# short user_len; // username string length
|
|
# short user_len; // username string length
|
|
# short user_off; // username string offset
|
|
# byte zero[2];
|
|
#
|
|
# short host_len; // host string length
|
|
# short host_len; // host string length
|
|
# short host_off; // host string offset
|
|
# byte zero[6];
|
|
#
|
|
# short msg_len; // message length
|
|
# byte zero[2];
|
|
#
|
|
# short flags; // 0x8201
|
|
# byte zero[2];
|
|
#
|
|
# byte dom[*]; // domain string (unicode UTF-16LE)
|
|
# byte user[*]; // username string (unicode UTF-16LE)
|
|
# byte host[*]; // host string (unicode UTF-16LE)
|
|
# byte lm_resp[*]; // LanManager response
|
|
# byte nt_resp[*]; // NT response
|
|
# } type_3_message
|
|
#
|
|
def self.process_type3_message(message)
|
|
decode = Rex::Text.decode_base64(message.strip)
|
|
type = decode[8,1].unpack("C").first
|
|
if (type == 3)
|
|
lm_len = decode[12,2].unpack("v").first
|
|
lm_offset = decode[16,2].unpack("v").first
|
|
lm = decode[lm_offset, lm_len].unpack("H*").first
|
|
|
|
nt_len = decode[20,2].unpack("v").first
|
|
nt_offset = decode[24,2].unpack("v").first
|
|
nt = decode[nt_offset, nt_len].unpack("H*").first
|
|
|
|
dom_len = decode[28,2].unpack("v").first
|
|
dom_offset = decode[32,2].unpack("v").first
|
|
domain = decode[dom_offset, dom_len]
|
|
|
|
user_len = decode[36,2].unpack("v").first
|
|
user_offset = decode[40,2].unpack("v").first
|
|
user = decode[user_offset, user_len]
|
|
|
|
host_len = decode[44,2].unpack("v").first
|
|
host_offset = decode[48,2].unpack("v").first
|
|
host = decode[host_offset, host_len]
|
|
|
|
return domain, user, host, lm, nt
|
|
else
|
|
return "", "", "", "", ""
|
|
end
|
|
end
|
|
|
|
#
|
|
# Process Type 1 NTLM Messages, return a Base64 Type 2 Message
|
|
#
|
|
def self.process_type1_message(message, nonce = "\x11\x22\x33\x44\x55\x66\x77\x88", win_domain = 'DOMAIN',
|
|
win_name = 'SERVER', dns_name = 'server', dns_domain = 'example.com', downgrade = true)
|
|
|
|
dns_name = Rex::Text.to_unicode(dns_name + "." + dns_domain)
|
|
win_domain = Rex::Text.to_unicode(win_domain)
|
|
dns_domain = Rex::Text.to_unicode(dns_domain)
|
|
win_name = Rex::Text.to_unicode(win_name)
|
|
decode = Rex::Text.decode_base64(message.strip)
|
|
|
|
type = decode[8,1].unpack("C").first
|
|
|
|
if (type == 1)
|
|
# A type 1 message has been received, lets build a type 2 message response
|
|
|
|
reqflags = decode[12,4]
|
|
reqflags = reqflags.unpack("V").first
|
|
|
|
if (reqflags & CONST::REQUEST_TARGET) == CONST::REQUEST_TARGET
|
|
|
|
if (downgrade)
|
|
# At this time NTLMv2 and signing requirements are not supported
|
|
if (reqflags & CONST::NEGOTIATE_NTLM2_KEY) == CONST::NEGOTIATE_NTLM2_KEY
|
|
reqflags = reqflags - CONST::NEGOTIATE_NTLM2_KEY
|
|
end
|
|
if (reqflags & CONST::NEGOTIATE_ALWAYS_SIGN) == CONST::NEGOTIATE_ALWAYS_SIGN
|
|
reqflags = reqflags - CONST::NEGOTIATE_ALWAYS_SIGN
|
|
end
|
|
end
|
|
|
|
flags = reqflags + CONST::TARGET_TYPE_DOMAIN + CONST::TARGET_TYPE_SERVER
|
|
tid = true
|
|
|
|
tidoffset = 48 + win_domain.length
|
|
tidbuff =
|
|
[2].pack('v') + # tid type, win domain
|
|
[win_domain.length].pack('v') +
|
|
win_domain +
|
|
[1].pack('v') + # tid type, server name
|
|
[win_name.length].pack('v') +
|
|
win_name +
|
|
[4].pack('v') + # tid type, domain name
|
|
[dns_domain.length].pack('v') +
|
|
dns_domain +
|
|
[3].pack('v') + # tid type, dns_name
|
|
[dns_name.length].pack('v') +
|
|
dns_name
|
|
else
|
|
flags = CONST::NEGOTIATE_UNICODE + CONST::NEGOTIATE_NTLM
|
|
tid = false
|
|
end
|
|
|
|
type2msg = "NTLMSSP\0" + # protocol, 8 bytes
|
|
"\x02\x00\x00\x00" # type, 4 bytes
|
|
|
|
if (tid)
|
|
type2msg += # Target security info, 8 bytes. Filled if REQUEST_TARGET
|
|
[win_domain.length].pack('v') + # Length, 2 bytes
|
|
[win_domain.length].pack('v') # Allocated space, 2 bytes
|
|
end
|
|
|
|
type2msg +="\x30\x00\x00\x00" + # Offset, 4 bytes
|
|
[flags].pack('V') + # flags, 4 bytes
|
|
nonce + # the nonce, 8 bytes
|
|
"\x00" * 8 # Context (all 0s), 8 bytes
|
|
|
|
if (tid)
|
|
type2msg += # Target information security buffer. Filled if REQUEST_TARGET
|
|
[tidbuff.length].pack('v') + # Length, 2 bytes
|
|
[tidbuff.length].pack('v') + # Allocated space, 2 bytes
|
|
[tidoffset].pack('V') + # Offset, 4 bytes (usually \x48 + length of win_domain)
|
|
win_domain + # Target name data (domain in unicode if REQUEST_UNICODE)
|
|
# Target information data
|
|
tidbuff + # Type, 2 bytes
|
|
# Length, 2 bytes
|
|
# Data (in unicode if REQUEST_UNICODE)
|
|
"\x00\x00\x00\x00" # Terminator, 4 bytes, all \x00
|
|
end
|
|
|
|
type2msg = Rex::Text.encode_base64(type2msg).delete("\n") # base64 encode and remove the returns
|
|
else
|
|
# This is not a Type2 message
|
|
type2msg = ""
|
|
end
|
|
|
|
return type2msg
|
|
end
|
|
|
|
#
|
|
# Downgrading Type messages to LMv1/NTLMv1 and removing signing
|
|
#
|
|
def self.downgrade_type_message(message)
|
|
decode = Rex::Text.decode_base64(message.strip)
|
|
|
|
type = decode[8,1].unpack("C").first
|
|
|
|
if (type > 0 and type < 4)
|
|
reqflags = decode[12..15] if (type == 1 or type == 3)
|
|
reqflags = decode[20..23] if (type == 2)
|
|
reqflags = reqflags.unpack("V")
|
|
|
|
# Remove NEGOTIATE_NTLMV2_KEY and NEGOTIATE_ALWAYS_SIGN, this lowers the negotiation
|
|
# down to LMv1/NTLMv1.
|
|
if (reqflags & CONST::NEGOTIATE_NTLM2_KEY) == CONST::NEGOTIATE_NTLM2_KEY
|
|
reqflags = reqflags - CONST::NEGOTIATE_NTLM2_KEY
|
|
end
|
|
if (reqflags & CONST::NEGOTIATE_ALWAYS_SIGN) == CONST::NEGOTIATE_ALWAYS_SIGN
|
|
reqflags = reqflags - CONST::NEGOTIATE_ALWAYS_SIGN
|
|
end
|
|
|
|
# Return the flags back to the decode so we can base64 it again
|
|
flags = reqflags.to_s(16)
|
|
0.upto(8) do |idx|
|
|
if (idx > flags.length)
|
|
flags.insert(0, "0")
|
|
end
|
|
end
|
|
|
|
idx = 0
|
|
0.upto(3) do |cnt|
|
|
if (type == 2)
|
|
decode[23-cnt] = [flags[idx,1]].pack("C")
|
|
else
|
|
decode[15-cnt] = [flags[idx,1]].pack("C")
|
|
end
|
|
idx += 2
|
|
end
|
|
|
|
end
|
|
return Rex::Text.encode_base64(decode).delete("\n") # base64 encode and remove the returns
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
end
|