Skip to content

Commit 5e543bc

Browse files
committed
adding capability to analyze and rip files from SMB traffic
1 parent cc15c85 commit 5e543bc

File tree

4 files changed

+1012
-0
lines changed

4 files changed

+1012
-0
lines changed

decoders/smb/psexec.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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()

decoders/smb/rip-smb-uploads.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
2015 Feb 13
3+
4+
Goes through SMB traffic and snips out any file uploads it sees.
5+
6+
Specifically, it looks for create, write, and close commands and creates a
7+
local file, writes the raw data to the local file, and closes the file,
8+
respectively.
9+
"""
10+
11+
import dshell
12+
from smbdecoder import SMBDecoder
13+
import sys
14+
import util
15+
import os
16+
17+
SMB_STATUS_SUCCESS = 0x0
18+
SMB_COM_OPEN = 0x02 # Open a file.
19+
SMB_COM_CLOSE = 0x04 # Close a file.
20+
SMB_COM_NT_CREATE_ANDX = 0xa2 # Create or open a file or a directory.
21+
SMB_COM_WRITE_ANDX = 0x2f # Extended file write with AndX chaining.
22+
23+
24+
class DshellDecoder(SMBDecoder):
25+
26+
def __init__(self):
27+
self.fidhandles = {} # dictionary to map fid handles to filenames
28+
# dictionary to map fid handles to local filedescriptors
29+
# (ie. fd = open(fname,'wb'))
30+
self.fds = {}
31+
self.outdir = None
32+
SMBDecoder.__init__(self,
33+
name='rip-smb-uploads',
34+
description='Extract files uploaded via SMB',
35+
filter='tcp and port 445',
36+
filterfn=lambda t: t[0][1] == 445 or t[1][1] == 445,
37+
author='bg',
38+
optiondict={
39+
"outdir": {"help": "Directory to place files (default: ./smb_out)", "default": "./smb_out", "metavar": "DIRECTORY"},
40+
}
41+
)
42+
self.legacy = True
43+
44+
def preModule(self):
45+
if not os.path.exists(self.outdir):
46+
try:
47+
os.makedirs(self.outdir)
48+
except OSError as e:
49+
self.error("Could not create directory '%s'\n%s" % (self.outdir, e))
50+
sys.exit(1)
51+
52+
def SMBHandler(self, conn, request=None, response=None, requesttime=None, responsetime=None, cmd=None, status=None):
53+
# we only care about valid responses and matching request/response user
54+
# IDs
55+
if status == SMB_STATUS_SUCCESS and request.uid == response.uid:
56+
57+
if cmd == SMB_COM_NT_CREATE_ANDX: # file is being requested/opened
58+
self.debug('%s UID: %s MID: %s NT Create AndX Status: %s' % (
59+
conn.addr, request.uid, response.mid, hex(status)))
60+
filename = request.PARSE_NT_CREATE_ANDX_REQUEST(
61+
request.smbdata)
62+
if type(filename) == type(None):
63+
self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_REQUEST\n%s' % util.hexPlusAscii(request.smbdata))
64+
return
65+
66+
fid = response.PARSE_NT_CREATE_ANDX_RESPONSE(response.smbdata)
67+
self.debug('%s FID: %s' % (conn.addr, fid))
68+
69+
if fid == -1:
70+
self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_RESPONSE\n%s' % util.hexPlusAscii(response.smbdata))
71+
self.debug(util.hexPlusAscii(response.smbdata))
72+
return
73+
self.fidhandles[fid] = self.__localfilename(self.outdir, os.path.normpath(filename))
74+
75+
elif cmd == SMB_COM_WRITE_ANDX: # write data to the file
76+
fid, rawbytes = request.PARSE_WRITE_ANDX(request.smbdata)
77+
78+
# do we have a local fd already open to handle this write?
79+
if fid in self.fds.keys():
80+
self.fds[fid].write(rawbytes)
81+
else:
82+
try:
83+
fidhandle = self.fidhandles[fid]
84+
self.fds[fid] = open(fidhandle, 'wb')
85+
self.fds[fid].write(rawbytes)
86+
except KeyError:
87+
self.debug("Error: Could not find fidhandle for FID %s" % (fid))
88+
return
89+
90+
elif cmd == SMB_COM_CLOSE: # file is being closed
91+
fid = request.PARSE_COM_CLOSE(request.smbdata)
92+
if fid in self.fds.keys():
93+
self.log(repr(conn) + '\t%s' % (self.fidhandles[fid]))
94+
self.fds[fid].close()
95+
del self.fds[fid]
96+
if fid in self.fidhandles.keys():
97+
self.debug('Closing FID: %s Filename: %s' %
98+
(hex(fid), self.fidhandles[fid]))
99+
del self.fidhandles[fid]
100+
101+
102+
def __localfilename(self, path, origname):
103+
# Generates a local file name based on the original
104+
tmp = origname.replace("\\", "_")
105+
tmp = tmp.replace("/", "_")
106+
tmp = tmp.replace(":", "_")
107+
localname = ''
108+
for c in tmp:
109+
if ord(c) > 32 and ord(c) < 127:
110+
localname += c
111+
else:
112+
localname += "%%%02X" % ord(c)
113+
localname = os.path.join(path, localname)
114+
postfix = ''
115+
i = 0
116+
while os.path.exists(localname + postfix):
117+
i += 1
118+
postfix = "_%02d" % i
119+
return localname + postfix
120+
121+
122+
if __name__ == '__main__':
123+
dObj = DshellDecoder()
124+
print dObj
125+
else:
126+
dObj = DshellDecoder()

0 commit comments

Comments
 (0)