|
| 1 | +""" |
| 2 | +2015 Feb 13 |
| 3 | +
|
| 4 | +Processes SMB traffic and attempts to extract command/response information |
| 5 | +from psexec. |
| 6 | +
|
| 7 | +When a successful SMB connection is seen and it matches a psexec regular |
| 8 | +expression, it creates a new "psexec" object to store connection information |
| 9 | +and messages. |
| 10 | +
|
| 11 | +Once the connection closes, an alert is generated (of configurable verbosity) |
| 12 | +relaying basic information and messages passed. |
| 13 | +""" |
| 14 | + |
| 15 | +#import dshell |
| 16 | +from smbdecoder import SMBDecoder |
| 17 | +import colorout |
| 18 | +import util |
| 19 | +import re |
| 20 | +import datetime |
| 21 | + |
| 22 | +SMB_STATUS_SUCCESS = 0x0 |
| 23 | +SMB_COM_OPEN = 0x02 # Open a file. |
| 24 | +SMB_COM_CLOSE = 0x04 # Close a file. |
| 25 | +SMB_COM_NT_CREATE_ANDX = 0xa2 # Create or open a file or a directory. |
| 26 | +SMB_COM_WRITE_ANDX = 0x2f # Extended file write with AndX chaining. |
| 27 | +SMB_COM_READ_ANDX = 0x2E |
| 28 | +SMB_COM_SESSION_SETUP_ANDX = 0x73 |
| 29 | + |
| 30 | + |
| 31 | +class DshellDecoder(SMBDecoder): |
| 32 | + |
| 33 | + def __init__(self): |
| 34 | + # dictionary indexed by uid, points to login domain\name (string) |
| 35 | + self.uidname = {} |
| 36 | + self.fidhandles = {} # dictionary to map fid handles to psexec objects |
| 37 | + # dictionary of psexec objects, indexed by conn+PID (use sessIndex |
| 38 | + # function) |
| 39 | + self.psexecobjs = {} |
| 40 | + # FID won't work as an index because each stream has its own |
| 41 | + SMBDecoder.__init__(self, |
| 42 | + name='psexec', |
| 43 | + description='Extract command/response information from psexec over smb', |
| 44 | + filter='tcp and (port 445 or port 139)', |
| 45 | + filterfn=lambda t: t[0][1] == 445 or t[1][1] == 445 or t[0][1] == 139 or t[1][1] == 139, |
| 46 | + author='amm', |
| 47 | + optiondict={ |
| 48 | + 'alertsonly': {'action': 'store_true', 'help': 'only dump alerts, not content'}, |
| 49 | + 'htmlalert': {'action': 'store_true', 'help': 'include html as named value in alerts'}, |
| 50 | + 'time': {'action': 'store_true', 'help': 'display command/response timestamps'} |
| 51 | + } |
| 52 | + ) |
| 53 | + self.legacy = True |
| 54 | + # self.out=colorout.ColorOutput(title='psexec') |
| 55 | + self.output = 'colorout' |
| 56 | + |
| 57 | + def sessIndexFromPID(self, conn, pid): |
| 58 | + return ':'.join((str(conn.starttime), conn.sip, str(conn.sport), conn.dip, str(conn.dport), pid)) |
| 59 | + |
| 60 | + def connectionHandler(self, conn): |
| 61 | + SMBDecoder.connectionHandler(self, conn) |
| 62 | + for k in self.psexecobjs.keys(): |
| 63 | + del self.psexecobjs[k] |
| 64 | + |
| 65 | + # |
| 66 | + # Internal class to contain psexec session information |
| 67 | + # |
| 68 | + class psexec: |
| 69 | + |
| 70 | + def __init__(self, parent, conn, hostname, pid, opentime): |
| 71 | + self.parent = parent |
| 72 | + self.conn = conn |
| 73 | + self.hostname = hostname |
| 74 | + self.pid = pid |
| 75 | + self.opentime = opentime |
| 76 | + self.closetime = conn.endtime |
| 77 | + self.username = '' |
| 78 | + self.open_iohandles = {} # indexed by FID, points to filename |
| 79 | + self.closed_iohandles = {} |
| 80 | + self.msgList = [] # List of tuples (text, direction) |
| 81 | + self.csCount = 0 |
| 82 | + self.scCount = 0 |
| 83 | + self.csBytes = 0 |
| 84 | + self.scBytes = 0 |
| 85 | + self.lastDirection = '' |
| 86 | + |
| 87 | + def addmsg(self, text, direction, ts): |
| 88 | + # Only store timestamp information if this is a change in direction |
| 89 | + if direction == self.lastDirection: |
| 90 | + self.msgList.append((text, direction, None)) |
| 91 | + else: |
| 92 | + self.msgList.append((text, direction, ts)) |
| 93 | + self.lastDirection = direction |
| 94 | + if direction == 'cs': |
| 95 | + self.csCount += 1 |
| 96 | + self.csBytes += len(text) |
| 97 | + elif direction == 'sc': |
| 98 | + self.scCount += 1 |
| 99 | + self.scBytes += len(text) |
| 100 | + |
| 101 | + def addIO(self, fid, name): |
| 102 | + if fid in self.open_iohandles: |
| 103 | + self.parent.warn("IO Handle with FID %s (%s) is already associated with psexec session %d" % ( |
| 104 | + hex(fid), name, self.pid)) |
| 105 | + self.open_iohandles[fid] = name |
| 106 | + |
| 107 | + def delIO(self, fid): |
| 108 | + if fid in self.open_iohandles: |
| 109 | + self.closed_iohandles[fid] = self.open_iohandles[fid] |
| 110 | + del self.open_iohandles[fid] |
| 111 | + |
| 112 | + def handleCount(self): |
| 113 | + return len(self.open_iohandles) |
| 114 | + # |
| 115 | + # Long output (screen/html) |
| 116 | + # |
| 117 | + |
| 118 | + def write(self, out=None): |
| 119 | + if out == None: |
| 120 | + out = self.parent.out |
| 121 | + out.write("PSEXEC Service from host %s with PID %s\n" % |
| 122 | + (self.hostname, self.pid), formatTag='H1') |
| 123 | + if len(self.username): |
| 124 | + out.write("User: %s\n" % (self.username), formatTag='H2') |
| 125 | + out.write("Start: %s UTC\n End: %s UTC\n" % (datetime.datetime.utcfromtimestamp( |
| 126 | + self.conn.starttime), datetime.datetime.utcfromtimestamp(self.conn.endtime)), formatTag='H2') |
| 127 | + out.write("%s:%s -> %s:%s\n" % (self.conn.clientip, self.conn.clientport, |
| 128 | + self.conn.serverip, self.conn.serverport), formatTag="H2", direction="cs") |
| 129 | + out.write("%s:%s -> %s:%s\n\n" % (self.conn.serverip, self.conn.serverport, |
| 130 | + self.conn.clientip, self.conn.clientport), formatTag="H2", direction="sc") |
| 131 | + for msg in self.msgList: |
| 132 | + out.write( |
| 133 | + msg[0], direction=msg[1], timestamp=msg[2], time=self.parent.time) |
| 134 | + out.write("\n") |
| 135 | + # |
| 136 | + # Short output (alert) |
| 137 | + # |
| 138 | + |
| 139 | + def alert(self): |
| 140 | + kwargs = {'hostname': self.hostname, 'pid': self.pid, 'username': self.username, |
| 141 | + 'opentime': self.opentime, 'closetime': self.closetime, |
| 142 | + 'csCount': self.csCount, 'scCount': self.scCount, 'csBytes': self.csBytes, 'scBytes': self.scBytes} |
| 143 | + if self.parent.htmlalert: |
| 144 | + htmlfactory = colorout.ColorOutput( |
| 145 | + htmlgenerator=True, title="psexec") |
| 146 | + self.write(htmlfactory) |
| 147 | + htmlfactory.close() |
| 148 | + kwargs['html'] = htmlfactory.htmldump() |
| 149 | + kwargs.update(self.conn.info()) |
| 150 | + kwargs['ts'] = self.opentime |
| 151 | + self.parent.alert( |
| 152 | + "Host: %s, PID: %s, CS: %d, SC: %d, User: %s" % ( |
| 153 | + self.hostname, self.pid, self.csBytes, self.scBytes, self.username), |
| 154 | + kwargs |
| 155 | + ) |
| 156 | + |
| 157 | + def __del__(self): |
| 158 | + if self.parent.alertsonly: |
| 159 | + self.alert() |
| 160 | + else: |
| 161 | + self.write() |
| 162 | + |
| 163 | + def SMBHandler(self, conn, request=None, response=None, requesttime=None, responsetime=None, cmd=None, status=None): |
| 164 | + # we only care about valid responses and matching request/response user |
| 165 | + # IDs |
| 166 | + if status == SMB_STATUS_SUCCESS and request.uid == response.uid: |
| 167 | + |
| 168 | + if cmd == SMB_COM_SESSION_SETUP_ANDX and type(status) != type(None): |
| 169 | + auth_record = request.PARSE_SESSION_SETUP_ANDX_REQUEST( |
| 170 | + request.smbdata) |
| 171 | + if not(auth_record): |
| 172 | + return |
| 173 | + domain_name = auth_record.domain_name |
| 174 | + user_name = auth_record.user_name |
| 175 | + self.uidname[response.uid] = "%s\\%s" % ( |
| 176 | + domain_name, user_name) |
| 177 | + |
| 178 | + # file is being requested/opened |
| 179 | + elif cmd == SMB_COM_NT_CREATE_ANDX: |
| 180 | + self.debug('%s UID: %s MID: %s NT Create AndX Status: %s' % ( |
| 181 | + conn.addr, request.uid, response.mid, hex(status))) |
| 182 | + filename = request.PARSE_NT_CREATE_ANDX_REQUEST( |
| 183 | + request.smbdata) |
| 184 | + if type(filename) == type(None): |
| 185 | + self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_REQUEST\n%s' % util.hexPlusAscii( |
| 186 | + request.smbdata)) |
| 187 | + return |
| 188 | + |
| 189 | + fid = response.PARSE_NT_CREATE_ANDX_RESPONSE(response.smbdata) |
| 190 | + |
| 191 | + if fid == -1: |
| 192 | + self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_RESPONSE\n%s' % util.hexPlusAscii( |
| 193 | + response.smbdata)) |
| 194 | + self.debug(util.hexPlusAscii(response.smbdata)) |
| 195 | + return |
| 196 | + match = re.search( |
| 197 | + r'psexecsvc-(.*)-(\d+)-(stdin|stdout|stderr)', filename) |
| 198 | + if not match: |
| 199 | + return |
| 200 | + |
| 201 | + # We have a PSEXEC File Handle! |
| 202 | + hostname = match.group(1) |
| 203 | + pid = match.group(2) |
| 204 | + iohandleName = match.group(3) |
| 205 | + sessionIndex = self.sessIndexFromPID(conn, pid) |
| 206 | + if not sessionIndex in self.psexecobjs: |
| 207 | + self.psexecobjs[sessionIndex] = self.psexec( |
| 208 | + self, conn, hostname, pid, requesttime) |
| 209 | + self.fidhandles[fid] = self.psexecobjs[sessionIndex] |
| 210 | + self.fidhandles[fid].addIO(fid, filename) |
| 211 | + if response.uid in self.uidname: |
| 212 | + self.fidhandles[fid].username = self.uidname[response.uid] |
| 213 | + |
| 214 | + elif cmd == SMB_COM_WRITE_ANDX: # write data to the file |
| 215 | + fid, rawbytes = request.PARSE_WRITE_ANDX(request.smbdata) |
| 216 | + self.debug('COM_WRITE_ANDX\n%s' % |
| 217 | + (util.hexPlusAscii(request.smbdata))) |
| 218 | + if fid in self.fidhandles: |
| 219 | + self.fidhandles[fid].addmsg(rawbytes, 'cs', requesttime) |
| 220 | + |
| 221 | + elif cmd == SMB_COM_READ_ANDX: # write data to the file |
| 222 | + fid = request.PARSE_READ_ANDX_Request(request.smbdata) |
| 223 | + rawbytes = response.PARSE_READ_ANDX_Response(response.smbdata) |
| 224 | + self.debug('COM_READ_ANDX (FID %s)\n%s' % |
| 225 | + (fid, util.hexPlusAscii(response.smbdata))) |
| 226 | + if fid in self.fidhandles: |
| 227 | + self.fidhandles[fid].addmsg(rawbytes, 'sc', responsetime) |
| 228 | + |
| 229 | + elif cmd == SMB_COM_CLOSE: # file is being closed |
| 230 | + fid = request.PARSE_COM_CLOSE(request.smbdata) |
| 231 | + if fid in self.fidhandles.keys(): |
| 232 | + self.fidhandles[fid].delIO(fid) |
| 233 | + self.debug('Closing FID: %s Filename: %s' % |
| 234 | + (hex(fid), self.fidhandles[fid])) |
| 235 | + if self.fidhandles[fid].handleCount() < 1 and self.sessIndexFromPID(conn, self.fidhandles[fid].pid) in self.psexecobjs: |
| 236 | + self.psexecobjs[ |
| 237 | + self.sessIndexFromPID(conn, self.fidhandles[fid].pid)].closetime = responsetime |
| 238 | + del self.psexecobjs[ |
| 239 | + self.sessIndexFromPID(conn, self.fidhandles[fid].pid)] |
| 240 | + del self.fidhandles[fid] |
| 241 | + |
| 242 | + |
| 243 | +if __name__ == '__main__': |
| 244 | + dObj = DshellDecoder() |
| 245 | + print dObj |
| 246 | +else: |
| 247 | + dObj = DshellDecoder() |
0 commit comments