diff --git a/src/core/tds.py b/src/core/tds.py new file mode 100644 index 000000000..53f983a51 --- /dev/null +++ b/src/core/tds.py @@ -0,0 +1,1189 @@ +#!/usr/bin/python +# Copyright (c) 2003-2012 CORE Security Technologies +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# $Id: tds.py 632 2012-07-26 22:18:33Z bethus@gmail.com $ +# +# Description: [MS-TDS] & [MC-SQLR] implementation. +# +# ToDo: +# [ ] Add all the tokens left +# [ ] parseRow should be rewritten and add support for all the SQL types in a +# good way. Right now it just supports a few types. +# [ ] printRows is crappy, just an easy way to print the rows. It should be +# rewritten to output like a normal SQL client +# +# Author: +# Alberto Solino (beto@coresecurity.com) +# + + +from impacket import ntlm, uuid +from impacket.structure import Structure +import random +import string +import struct +import socket, select +import random +import binascii +import math +import datetime + + +# MC-SQLR Constants and Structures +SQLR_PORT = 1434 +SQLR_CLNT_BCAST_EX = 0x02 +SQLR_CLNT_UCAST_EX = 0x03 +SQLR_CLNT_UCAST_INST= 0x04 +SQLR_CLNT_UCAST_DAC = 0x0f + + +class SQLR(Structure): + commonHdr = ( + ('OpCode','B'), + ) + +class SQLR_UCAST_INST(SQLR): + structure = ( + ('Instance',':') + ) + def __init__(self, data = None): + SQLR.__init__(self,data) + if data is not None: + self['OpCode'] = SQLR_CLNT_UCAST_INST + +class SQLR_UCAST_DAC(SQLR): + structure = ( + ('Protocol', 'B=1'), + ('Instance', ':'), + ) + def __init__(self, data = None): + SQLR.__init__(self,data) + if data is not None: + self['OpCode'] = SQLR_CLNT_UCAST_DAC + +class SQLR_Response(SQLR): + structure = ( + ('Size','H=8+len(Data)'), + ('SPID','>H=0'), + ('PacketID','B=0'), + ('VersionOffset','>H'), + ('VersionLength','>H=len(self["Version"])'), + ('EncryptionToken','>B=0x1'), + ('EncryptionOffset','>H'), + ('EncryptionLength','>H=1'), + ('InstanceToken','>B=2'), + ('InstanceOffset','>H'), + ('InstanceLength','>H=len(self["Instance"])'), + ('ThreadIDToken','>B=3'), + ('ThreadIDOffset','>H'), + ('ThreadIDLength','>H=4'), + ('EndToken','>B=0xff'), + ('_Version','_-Version','self["VersionLength"]'), + ('Version',':'), + ('Encryption','B'), + ('_Instance','_-Instance','self["InstanceLength"]-1'), + ('Instance',':'), + ('ThreadID',':'), + ) + + def __str__(self): + self['VersionOffset']=21 + self['EncryptionOffset']=self['VersionOffset'] + len(self['Version']) + self['InstanceOffset']=self['EncryptionOffset'] + 1 + self['ThreadIDOffset']=self['InstanceOffset'] + len(self['Instance']) + return Structure.__str__(self) + +class TDS_LOGIN(Structure): + structure = ( + ('Length','L=0x71'), + ('PacketSize','>L=32766'), + ('ClientProgVer','>L=7'), + ('ClientPID','> 4) ^ 0xa5) , password)) + + def connect(self): + af, socktype, proto, canonname, sa = socket.getaddrinfo(self.server, self.port, 0, socket.SOCK_STREAM)[0] + sock = socket.socket(af, socktype, proto) + sock.connect(sa) + self.socket = sock + return sock + + def disconnect(self): + return self.socket.close() + + def setPacketSize(self, packetSize): + self.packetSize = packetSize + + def getPacketSize(self): + return self.packetSize + + def sendTDS(self, packetType, data, packetID = 1): + if (len(data)-8) > self.packetSize: + remaining = data[self.packetSize-8:] + tds = TDSPacket() + tds['Type'] = packetType + tds['Status'] = TDS_STATUS_NORMAL + tds['PacketID'] = packetID + tds['Data'] = data[:self.packetSize-8] + self.socket.sendall(str(tds)) + while len(remaining) > (self.packetSize-8): + packetID += 1 + tds['PacketID'] = packetID + tds['Data'] = remaining[:self.packetSize-8] + self.socket.sendall(str(tds)) + remaining = remaining[self.packetSize-8:] + data = remaining + packetID+=1 + + tds = TDSPacket() + tds['Type'] = packetType + tds['Status'] = TDS_STATUS_EOM + tds['PacketID'] = packetID + tds['Data'] = data + self.socket.sendall(str(tds)) + + def recvTDS(self, packetSize = None): + # Do reassembly here + if packetSize is None: + packetSize = self.packetSize + packet = TDSPacket(self.socket.recv(packetSize)) + status = packet['Status'] + packetLen = packet['Length']-8 + while packetLen > len(packet['Data']): + data = self.socket.recv(packetSize) + packet['Data'] += data + + remaining = None + if packetLen < len(packet['Data']): + remaining = packet['Data'][packetLen:] + packet['Data'] = packet['Data'][:packetLen] + + #print "REMAINING ", + #if remaining is None: + # print None + #else: + # print len(remaining) + + while status != TDS_STATUS_EOM: + if remaining is not None: + tmpPacket = TDSPacket(remaining) + remaining = None + else: + tmpPacket = TDSPacket(self.socket.recv(packetSize)) + + packetLen = tmpPacket['Length'] - 8 + while packetLen > len(tmpPacket['Data']): + data = self.socket.recv(packetSize) + tmpPacket['Data'] += data + + remaining = None + if packetLen < len(tmpPacket['Data']): + remaining = tmpPacket['Data'][packetLen:] + tmpPacket['Data'] = tmpPacket['Data'][:packetLen] + + status = tmpPacket['Status'] + packet['Data'] += tmpPacket['Data'] + packet['Length'] += tmpPacket['Length'] - 8 + + #print packet['Length'] + return packet + + def login(self, database, username, password='', domain='', hashes = None, useWindowsAuth = False): + + if hashes is not None: + lmhash, nthash = hashes.split(':') + lmhash = binascii.a2b_hex(lmhash) + nthash = binascii.a2b_hex(nthash) + else: + lmhash = '' + nthash = '' + + resp = self.preLogin() + + # Test this! + if resp['Encryption'] != TDS_ENCRYPT_NOT_SUP: + print "Encryption not supported" + + login = TDS_LOGIN() + + login['HostName'] = (''.join([random.choice(string.letters) for i in range(8)])).encode('utf-16le') + login['AppName'] = (''.join([random.choice(string.letters) for i in range(8)])).encode('utf-16le') + login['ServerName'] = self.server.encode('utf-16le') + login['CltIntName'] = login['AppName'] + login['ClientPID'] = random.randint(0,1024) + if database is not None: + login['Database'] = database.encode('utf-16le') + login['OptionFlags2'] = TDS_INIT_LANG_FATAL | TDS_ODBC_ON + + if useWindowsAuth is True: + login['OptionFlags2'] |= TDS_INTEGRATED_SECURITY_ON + # NTLMSSP Negotiate + auth = ntlm.getNTLMSSPType1('WORKSTATION','') + login['SSPI'] = str(auth) + else: + login['UserName'] = username.encode('utf-16le') + login['Password'] = self.encryptPassword(password.encode('utf-16le')) + login['SSPI'] = '' + + login['Length'] = len(str(login)) + + self.sendTDS(TDS_LOGIN7, str(login)) + # Send the NTLMSSP Negotiate or SQL Auth Packet + tds = self.recvTDS() + + if useWindowsAuth is True: + serverChallenge = tds['Data'][3:] + + # Generate the NTLM ChallengeResponse AUTH + type3, exportedSessionKey = ntlm.getNTLMSSPType3(auth, serverChallenge, username, password, domain, lmhash, nthash) + + self.sendTDS(TDS_SSPI, str(type3)) + tds = self.recvTDS() + + self.replies = self.parseReply(tds['Data']) + + if self.replies.has_key(TDS_LOGINACK_TOKEN): + return True + else: + return False + + def processColMeta(self): + for col in self.colMeta: + if col['Type'] in [TDS_NVARCHARTYPE, TDS_NCHARTYPE, TDS_NTEXTTYPE]: + col['Length'] = col['TypeData']/2 + fmt = '%%-%ds' + elif col['Type'] in [TDS_GUIDTYPE]: + col['Length'] = 36 + fmt = '%%%ds' + elif col['Type'] in [TDS_DECIMALNTYPE,TDS_NUMERICNTYPE]: + col['Length'] = ord(col['TypeData'][0]) + fmt = '%%%ds' + elif col['Type'] in [TDS_DATETIMNTYPE]: + col['Length'] = 19 + fmt = '%%-%ds' + elif col['Type'] in [TDS_INT4TYPE, TDS_INTNTYPE]: + col['Length'] = 11 + fmt = '%%%ds' + elif col['Type'] in [TDS_FLTNTYPE, TDS_MONEYNTYPE]: + col['Length'] = 25 + fmt = '%%%ds' + elif col['Type'] in [TDS_BITNTYPE, TDS_BIGCHARTYPE]: + col['Length'] = col['TypeData'] + fmt = '%%%ds' + elif col['Type'] in [TDS_BIGBINARYTYPE, TDS_BIGVARBINTYPE]: + col['Length'] = col['TypeData'] * 2 + fmt = '%%%ds' + elif col['Type'] in [TDS_TEXTTYPE, TDS_BIGVARCHRTYPE]: + col['Length'] = col['TypeData'] + fmt = '%%-%ds' + else: + col['Length'] = 10 + fmt = '%%%ds' + + if len(col['Name']) > col['Length']: + col['Length'] = len(col['Name']) + elif col['Length'] > self.MAX_COL_LEN: + col['Length'] = self.MAX_COL_LEN + + col['Format'] = fmt % col['Length'] + + + def printColumnsHeader(self): + if len(self.colMeta) == 0: + return + for col in self.colMeta: + print col['Format'] % col['Name'] + self.COL_SEPARATOR, + print '' + for col in self.colMeta: + print '-'*col['Length'] + self.COL_SEPARATOR, + print '' + + def printRows(self): + if self.lastError is True: + return + self.processColMeta() + self.printColumnsHeader() + for row in self.rows: + for col in self.colMeta: + print col['Format'] % row[col['Name']] + self.COL_SEPARATOR, + print '' + + + def printReplies(self): + for keys in self.replies.keys(): + for i, key in enumerate(self.replies[keys]): + if key['TokenType'] == TDS_ERROR_TOKEN: + print "[!] ERROR(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le')) + self.lastError = True + + elif key['TokenType'] == TDS_INFO_TOKEN: + print "[*] INFO(%s): Line %d: %s" % (key['ServerName'].decode('utf-16le'), key['LineNumber'], key['MsgText'].decode('utf-16le')) + + elif key['TokenType'] == TDS_LOGINACK_TOKEN: + print "[*] ACK: Result: %s - %s (%d%d %d%d) " % (key['Interface'], key['ProgName'].decode('utf-16le'), key['MajorVer'], key['MinorVer'], key['BuildNumHi'], key['BuildNumLow']) + + elif key['TokenType'] == TDS_ENVCHANGE_TOKEN: + if key['Type'] in (TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE): + record = TDS_ENVCHANGE_VARCHAR(key['Data']) + if record['OldValue'] == '': + record['OldValue'] = 'None'.encode('utf-16le') + elif record['NewValue'] == '': + record['NewValue'] = 'None'.encode('utf-16le') + if key['Type'] == TDS_ENVCHANGE_DATABASE: + type = 'DATABASE' + elif key['Type'] == TDS_ENVCHANGE_LANGUAGE: + type = 'LANGUAGE' + elif key['Type'] == TDS_ENVCHANGE_CHARSET: + type = 'CHARSET' + elif key['Type'] == TDS_ENVCHANGE_PACKETSIZE: + type = 'PACKETSIZE' + else: + type = "%d" % key['Type'] + + print "[*] ENVCHANGE(%s): Old Value: %s, New Value: %s" % (type,record['OldValue'].decode('utf-16le'), record['NewValue'].decode('utf-16le')) + + def parseRow(self,token): + # TODO: This REALLY needs to be improved. Right now we don't support correctly all the data types + # help would be appreciated ;) + if len(token) == 1: + return 0 + row = {} + origDataLen = len(token['Data']) + data = token['Data'] + for col in self.colMeta: + type = col['Type'] + if (type == TDS_NVARCHARTYPE) |\ + (type == TDS_NCHARTYPE): + #print "NVAR 0x%x" % type + charLen = struct.unpack(' 0: + uu = data[:uuidLen] + value = uuid.bin_to_string(uu) + data = data[uuidLen:] + else: + value = 'NULL' + + elif (type == TDS_NTEXTTYPE) |\ + (type == TDS_IMAGETYPE) : + # Skip the pointer data + charLen = ord(data[0]) + if charLen == 0: + value = 'NULL' + data = data[1:] + else: + data = data[1+charLen+8:] + charLen = struct.unpack(' 0: + value = struct.unpack(fmt,data[:valueSize])[0] + data = data[valueSize:] + else: + value = 'NULL' + + elif type == TDS_MONEYNTYPE: + valueSize = ord(data[:1]) + if valueSize == 4: + fmt = ' 0: + value = struct.unpack(fmt,data[:valueSize])[0] + if valueSize == 4: + value = float(value) / math.pow(10,4) + else: + value = float(value >> 32) / math.pow(10,4) + data = data[valueSize:] + else: + value = 'NULL' + + + elif type == TDS_BIGCHARTYPE: + #print "BIGC" + charLen = struct.unpack(' 0: + dateBytes = data[:valueSize] + dateValue = struct.unpack(' 0: + isPositiveSign = ord(value[0]) + if (valueLen-1) == 2: + fmt = ' 0: + if valueSize == 1: + value = ord(data[:valueSize]) + else: + value = data[:valueSize] + else: + value = 'NULL' + data = data[valueSize:] + + elif (type == TDS_INTNTYPE): + valueSize = ord(data[:1]) + if valueSize == 1: + fmt = ' 0: + value = struct.unpack(fmt,data[:valueSize])[0] + data = data[valueSize:] + else: + value = 'NULL' + elif (type == TDS_SSVARIANTTYPE): + print "ParseRow: SQL Variant type not yet supported :(" + raise + else: + print "ParseROW: Unsupported data type: 0%x" % type + raise + row[col['Name']] = value + + + self.rows.append(row) + + return (origDataLen - len(data)) + + def parseColMetaData(self, token): + # TODO Add support for more data types! + count = token['Count'] + if count == 0xFFFF: + return 0 + + self.colMeta = [] + origDataLen = len(token['Data']) + data = token['Data'] + for i in range(count): + column = {} + userType = struct.unpack(' 0: + tokenID = struct.unpack('B',tokens[0])[0] + if tokenID == TDS_ERROR_TOKEN: + token = TDS_INFO_ERROR(tokens) + elif tokenID == TDS_RETURNSTATUS_TOKEN: + token = TDS_RETURNSTATUS(tokens) + elif tokenID == TDS_INFO_TOKEN: + token = TDS_INFO_ERROR(tokens) + elif tokenID == TDS_LOGINACK_TOKEN: + token = TDS_LOGIN_ACK(tokens) + elif tokenID == TDS_ENVCHANGE_TOKEN: + token = TDS_ENVCHANGE(tokens) + if token['Type'] is TDS_ENVCHANGE_PACKETSIZE: + record = TDS_ENVCHANGE_VARCHAR(token['Data']) + self.packetSize = string.atoi( record['NewValue'].decode('utf-16le') ) + elif token['Type'] is TDS_ENVCHANGE_DATABASE: + record = TDS_ENVCHANGE_VARCHAR(token['Data']) + self.currentDB = record['NewValue'].decode('utf-16le') + + elif (tokenID == TDS_DONEINPROC_TOKEN) |\ + (tokenID == TDS_DONEPROC_TOKEN): + token = TDS_DONEINPROC(tokens) + elif tokenID == TDS_ORDER_TOKEN: + token = TDS_ORDER(tokens) + elif tokenID == TDS_ROW_TOKEN: + #print "ROW" + token = TDS_ROW(tokens) + tokenLen = self.parseRow(token) + token['Data'] = token['Data'][:tokenLen] + elif tokenID == TDS_COLMETADATA_TOKEN: + #print "COLMETA" + token = TDS_COLMETADATA(tokens) + tokenLen = self.parseColMetaData(token) + token['Data'] = token['Data'][:tokenLen] + elif tokenID == TDS_DONE_TOKEN: + token = TDS_DONE(tokens) + else: + print "Unknown Token %x" % tokenID + return replies + + if replies.has_key(tokenID) is not True: + replies[tokenID] = list() + + replies[tokenID].append(token) + tokens = tokens[len(token):] + #print "TYPE 0x%x, LEN: %d" %(tokenID, len(token)) + #print repr(tokens[:10]) + + return replies + + def batch(self, cmd): + # First of all we clear the rows, colMeta and lastError + self.rows = [] + self.colMeta = [] + self.lastError = False + self.sendTDS(TDS_SQL_BATCH, (cmd+'\r\n').encode('utf-16le')) + tds = self.recvTDS() + self.replies = self.parseReply(tds['Data']) + return self.rows + + # Handy alias + sql_query = batch + + def changeDB(self, db): + if db != self.currentDB: + self.batch('use %s' % db) + self.printReplies() + + def RunSQL(self,db,sql_query, **kwArgs): + self.changeDB(db) + self.printReplies() + ret = self.batch(sql_query) + self.printReplies() + + return ret