Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Impacket - Collection of Python classes for working with network protocols. 

2# 

3# SECUREAUTH LABS. Copyright (C) 2018 SecureAuth Corporation. All rights reserved. 

4# 

5# This software is provided under a slightly modified version 

6# of the Apache Software License. See the accompanying LICENSE file 

7# for more information. 

8# 

9# Description: 

10# Socks Proxy for the IMAP Protocol 

11# 

12# A simple SOCKS server that proxies a connection to relayed IMAP connections 

13# 

14# Author: 

15# Dirk-jan Mollema (@_dirkjan) / Fox-IT (https://www.fox-it.com) 

16# 

17import base64 

18 

19from impacket import LOG 

20from impacket.examples.ntlmrelayx.servers.socksserver import SocksRelay 

21 

22# Besides using this base class you need to define one global variable when 

23# writing a plugin: 

24PLUGIN_CLASS = "IMAPSocksRelay" 

25EOL = '\r\n' 

26 

27class IMAPSocksRelay(SocksRelay): 

28 PLUGIN_NAME = 'IMAP Socks Plugin' 

29 PLUGIN_SCHEME = 'IMAP' 

30 

31 def __init__(self, targetHost, targetPort, socksSocket, activeRelays): 

32 SocksRelay.__init__(self, targetHost, targetPort, socksSocket, activeRelays) 

33 self.packetSize = 8192 

34 self.idleState = False 

35 self.shouldClose = True 

36 

37 @staticmethod 

38 def getProtocolPort(): 

39 return 143 

40 

41 def getServerCapabilities(self): 

42 for key in list(self.activeRelays.keys()): 

43 if key != 'data' and key != 'scheme': 

44 if 'protocolClient' in self.activeRelays[key]: 

45 return self.activeRelays[key]['protocolClient'].session.capabilities 

46 

47 def initConnection(self): 

48 pass 

49 

50 def skipAuthentication(self): 

51 self.socksSocket.sendall('* OK The Microsoft Exchange IMAP4 service is ready.'+EOL) 

52 

53 # Next should be the client requesting CAPABILITIES 

54 tag, cmd = self.recvPacketClient() 

55 if cmd.upper() == 'CAPABILITY': 

56 clientcapabilities = list(self.getServerCapabilities()) 

57 # Don't offer these AUTH options so the client won't use them 

58 blacklist = ['AUTH=GSSAPI', 'AUTH=NTLM', 'LOGINDISABLED'] 

59 for cap in blacklist: 

60 if cap in clientcapabilities: 

61 clientcapabilities.remove(cap) 

62 

63 # Offer PLAIN auth for specifying the username 

64 if 'AUTH=PLAIN' not in clientcapabilities: 

65 clientcapabilities.append('AUTH=PLAIN') 

66 # Offer LOGIN for specifying the username 

67 if 'LOGIN' not in clientcapabilities: 

68 clientcapabilities.append('LOGIN') 

69 

70 LOG.debug('IMAP: Sending mirrored capabilities from server: %s' % ' '.join(clientcapabilities)) 

71 self.socksSocket.sendall('* CAPABILITY %s%s%s OK CAPABILITY completed.%s' % (' '.join(clientcapabilities), EOL, tag, EOL)) 

72 else: 

73 LOG.error('IMAP: Socks plugin expected CAPABILITY command, but got: %s' % cmd) 

74 return False 

75 # next 

76 tag, cmd = self.recvPacketClient() 

77 args = cmd.split(' ') 

78 if cmd.upper() == 'AUTHENTICATE PLAIN': 

79 # Send continuation command 

80 self.socksSocket.sendall('+'+EOL) 

81 # Client will now send their AUTH 

82 data = self.socksSocket.recv(self.packetSize) 

83 # This contains base64(\x00username\x00password), decode and split 

84 creds = base64.b64decode(data.strip()) 

85 self.username = creds.split('\x00')[1].upper() 

86 elif args[0].upper() == 'LOGIN': 

87 # Simple login 

88 self.username = args[1].upper() 

89 else: 

90 LOG.error('IMAP: Socks plugin expected LOGIN or AUTHENTICATE PLAIN command, but got: %s' % cmd) 

91 return False 

92 

93 # Check if we have a connection for the user 

94 if self.username in self.activeRelays: 

95 # Check the connection is not inUse 

96 if self.activeRelays[self.username]['inUse'] is True: 

97 LOG.error('IMAP: Connection for %s@%s(%s) is being used at the moment!' % ( 

98 self.username, self.targetHost, self.targetPort)) 

99 return False 

100 else: 

101 LOG.info('IMAP: Proxying client session for %s@%s(%s)' % ( 

102 self.username, self.targetHost, self.targetPort)) 

103 self.session = self.activeRelays[self.username]['protocolClient'].session 

104 else: 

105 LOG.error('IMAP: No session for %s@%s(%s) available' % ( 

106 self.username, self.targetHost, self.targetPort)) 

107 return False 

108 

109 # We arrived here, that means all is OK 

110 self.socksSocket.sendall('%s OK %s completed.%s' % (tag, args[0].upper(), EOL)) 

111 self.relaySocket = self.session.sock 

112 self.relaySocketFile = self.session.file 

113 return True 

114 

115 def tunnelConnection(self): 

116 keyword = '' 

117 tag = '' 

118 while True: 

119 try: 

120 data = self.socksSocket.recv(self.packetSize) 

121 except Exception as e: 

122 # Socks socket (client) closed connection or something else. Not fatal for killing the existing relay 

123 print((keyword, tag)) 

124 LOG.debug('IMAP: sockSocket recv(): %s' % (str(e))) 

125 break 

126 # If this returns with an empty string, it means the socket was closed 

127 if data == '': 

128 break 

129 # Set the new keyword, unless it is false, then break out of the function 

130 result = self.processTunnelData(keyword, tag, data) 

131 

132 if result is False: 

133 break 

134 # If its not false, it's a tuple with the keyword and tag 

135 keyword, tag = result 

136 

137 if tag != '': 

138 # Store the tag in the session so we can continue 

139 tag = int(tag) 

140 if self.idleState is True: 

141 self.relaySocket.sendall('DONE%s' % EOL) 

142 self.relaySocketFile.readline() 

143 

144 if self.shouldClose: 

145 tag +=1 

146 self.relaySocket.sendall('%s CLOSE%s' % (tag, EOL)) 

147 self.relaySocketFile.readline() 

148 

149 self.session.tagnum = tag+1 

150 

151 return 

152 

153 def processTunnelData(self, keyword, tag, data): 

154 # Pass the request to the server, store the tag unless the last command 

155 # was a continuation. In the case of the continuation we still check if 

156 # there were commands issued after 

157 analyze = data.split(EOL)[:-1] 

158 if keyword == '+': 

159 # We do send the continuation to the server 

160 # but we don't analyze it 

161 self.relaySocket.sendall(analyze.pop(0)+EOL) 

162 keyword = '' 

163 

164 for line in analyze: 

165 info = line.split(' ') 

166 tag = info[0] 

167 # See if a LOGOUT command was sent, in which case we want to close 

168 # the connection to the client but keep the relayed connection alive 

169 # also handle APPEND commands 

170 try: 

171 if info[1].upper() == 'IDLE': 

172 self.idleState = True 

173 elif info[1].upper() == 'DONE': 

174 self.idleState = False 

175 elif info[1].upper() == 'CLOSE': 

176 self.shouldClose = False 

177 elif info[1].upper() == 'LOGOUT': 

178 self.socksSocket.sendall('%s OK LOGOUT completed.%s' % (tag, EOL)) 

179 return False 

180 elif info[1].upper() == 'APPEND': 

181 LOG.debug('IMAP socks APPEND command detected, forwarding email data') 

182 # APPEND command sent, forward all the data, no further commands here 

183 self.relaySocket.sendall(data) 

184 sent = len(data) - len(line) + len(EOL) 

185 

186 # https://tools.ietf.org/html/rfc7888 

187 literal = info[4][1:-1] 

188 if literal[-1] == '+': 

189 literalPlus = True 

190 totalSize = int(literal[:-1]) 

191 else: 

192 literalPlus = False 

193 totalSize = int(literal) 

194 

195 while sent < totalSize: 

196 data = self.socksSocket.recv(self.packetSize) 

197 self.relaySocket.sendall(data) 

198 sent += len(data) 

199 LOG.debug('Forwarded %d bytes' % sent) 

200 

201 if literalPlus: 

202 data = self.socksSocket.recv(self.packetSize) 

203 self.relaySocket.sendall(data) 

204 

205 LOG.debug('IMAP socks APPEND command complete') 

206 # break out of the analysis loop 

207 break 

208 except IndexError: 

209 pass 

210 self.relaySocket.sendall(line+EOL) 

211 

212 # Send the response back to the client, until the command is complete 

213 # or the server requests more data 

214 while keyword != tag and keyword != '+': 

215 try: 

216 data = self.relaySocketFile.readline() 

217 except Exception as e: 

218 # This didn't break the connection to the server, don't make it fatal 

219 LOG.debug("IMAP relaySocketFile: %s" % str(e)) 

220 return False 

221 keyword = data.split(' ', 2)[0] 

222 try: 

223 self.socksSocket.sendall(data) 

224 except Exception as e: 

225 LOG.debug("IMAP socksSocket: %s" % str(e)) 

226 return False 

227 

228 # Return the keyword to indicate processing was OK 

229 return (keyword, tag) 

230 

231 

232 def recvPacketClient(self): 

233 data = self.socksSocket.recv(self.packetSize) 

234 space = data.find(' ') 

235 return (data[:space], data[space:].strip())