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# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. 

2# 

3# This software is provided under under a slightly modified version 

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

5# for more information. 

6# 

7# A Socks Proxy for the HTTP Protocol 

8# 

9# Author: 

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

11# 

12# Description: 

13# A simple SOCKS server that proxies a connection to relayed HTTP connections 

14# 

15# ToDo: 

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 = "HTTPSocksRelay" 

25EOL = b'\r\n' 

26 

27class HTTPSocksRelay(SocksRelay): 

28 PLUGIN_NAME = 'HTTP Socks Plugin' 

29 PLUGIN_SCHEME = 'HTTP' 

30 

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

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

33 self.packetSize = 8192 

34 

35 @staticmethod 

36 def getProtocolPort(): 

37 return 80 

38 

39 def initConnection(self): 

40 pass 

41 

42 def skipAuthentication(self): 

43 # See if the user provided authentication 

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

45 # Get headers from data 

46 headerDict = self.getHeaders(data) 

47 try: 

48 creds = headerDict['authorization'] 

49 if 'Basic' not in creds: 

50 raise KeyError() 

51 basicAuth = base64.b64decode(creds[6:]).decode("ascii") 

52 self.username = basicAuth.split(':')[0].upper() 

53 if '@' in self.username: 

54 # Workaround for clients which specify users with the full FQDN 

55 # such as ruler 

56 user, domain = self.username.split('@', 1) 

57 # Currently we only use the first part of the FQDN 

58 # this might break stuff on tools that do use an FQDN 

59 # where the domain NETBIOS name is not equal to the part 

60 # before the first . 

61 self.username = '%s/%s' % (domain.split('.')[0], user) 

62 

63 # Check if we have a connection for the user 

64 if self.username in self.activeRelays: 

65 # Check the connection is not inUse 

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

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

68 self.username, self.targetHost, self.targetPort)) 

69 return False 

70 else: 

71 LOG.info('HTTP: Proxying client session for %s@%s(%s)' % ( 

72 self.username, self.targetHost, self.targetPort)) 

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

74 else: 

75 LOG.error('HTTP: No session for %s@%s(%s) available' % ( 

76 self.username, self.targetHost, self.targetPort)) 

77 return False 

78 

79 except KeyError: 

80 # User didn't provide authentication yet, prompt for it 

81 LOG.debug('No authentication provided, prompting for basic authentication') 

82 reply = [b'HTTP/1.1 401 Unauthorized',b'WWW-Authenticate: Basic realm="ntlmrelayx - provide a DOMAIN/username"',b'Connection: close',b'',b''] 

83 self.socksSocket.send(EOL.join(reply)) 

84 return False 

85 

86 # When we are here, we have a session 

87 # Point our socket to the sock attribute of HTTPConnection 

88 # (contained in the session), which contains the socket 

89 self.relaySocket = self.session.sock 

90 # Send the initial request to the server 

91 tosend = self.prepareRequest(data) 

92 self.relaySocket.send(tosend) 

93 # Send the response back to the client 

94 self.transferResponse() 

95 return True 

96 

97 def getHeaders(self, data): 

98 # Get the headers from the request, ignore first "header" 

99 # since this is the HTTP method, identifier, version 

100 headerSize = data.find(EOL+EOL) 

101 headers = data[:headerSize].split(EOL)[1:] 

102 headers = [header.decode("ascii") for header in headers] 

103 headerDict = {hdrKey.split(':')[0].lower():hdrKey.split(':', 1)[1][1:] for hdrKey in headers} 

104 return headerDict 

105 

106 def transferResponse(self): 

107 data = self.relaySocket.recv(self.packetSize) 

108 headerSize = data.find(EOL+EOL) 

109 headers = self.getHeaders(data) 

110 try: 

111 bodySize = int(headers['content-length']) 

112 readSize = len(data) 

113 # Make sure we send the entire response, but don't keep it in memory 

114 self.socksSocket.send(data) 

115 while readSize < bodySize + headerSize + 4: 

116 data = self.relaySocket.recv(self.packetSize) 

117 readSize += len(data) 

118 self.socksSocket.send(data) 

119 except KeyError: 

120 try: 

121 if headers['transfer-encoding'] == 'chunked': 

122 # Chunked transfer-encoding, bah 

123 LOG.debug('Server sent chunked encoding - transferring') 

124 self.transferChunked(data, headers) 

125 else: 

126 # No body in the response, send as-is 

127 self.socksSocket.send(data) 

128 except KeyError: 

129 # No body in the response, send as-is 

130 self.socksSocket.send(data) 

131 

132 def transferChunked(self, data, headers): 

133 headerSize = data.find(EOL+EOL) 

134 

135 self.socksSocket.send(data[:headerSize + 4]) 

136 

137 body = data[headerSize + 4:] 

138 # Size of the chunk 

139 datasize = int(body[:body.find(EOL)], 16) 

140 while datasize > 0: 

141 # Size of the total body 

142 bodySize = body.find(EOL) + 2 + datasize + 2 

143 readSize = len(body) 

144 # Make sure we send the entire response, but don't keep it in memory 

145 self.socksSocket.send(body) 

146 while readSize < bodySize: 

147 maxReadSize = bodySize - readSize 

148 body = self.relaySocket.recv(min(self.packetSize, maxReadSize)) 

149 readSize += len(body) 

150 self.socksSocket.send(body) 

151 body = self.relaySocket.recv(self.packetSize) 

152 datasize = int(body[:body.find(EOL)], 16) 

153 LOG.debug('Last chunk received - exiting chunked transfer') 

154 self.socksSocket.send(body) 

155 

156 def prepareRequest(self, data): 

157 # Parse the HTTP data, removing headers that break stuff 

158 response = [] 

159 for part in data.split(EOL): 

160 # This means end of headers, stop parsing here 

161 if part == '': 

162 break 

163 # Remove the Basic authentication header 

164 if b'authorization' in part.lower(): 

165 continue 

166 # Don't close the connection 

167 if b'connection: close' in part.lower(): 

168 response.append('Connection: Keep-Alive') 

169 continue 

170 # If we are here it means we want to keep the header 

171 response.append(part) 

172 # Append the body 

173 response.append(b'') 

174 response.append(data.split(EOL+EOL)[1]) 

175 senddata = EOL.join(response) 

176 

177 # Check if the body is larger than 1 packet 

178 headerSize = data.find(EOL+EOL) 

179 headers = self.getHeaders(data) 

180 try: 

181 bodySize = int(headers['content-length']) 

182 readSize = len(data) 

183 while readSize < bodySize + headerSize + 4: 

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

185 readSize += len(data) 

186 senddata += data 

187 except KeyError: 

188 # No body, could be a simple GET or a POST without body 

189 # no need to check if we already have the full packet 

190 pass 

191 return senddata 

192 

193 

194 def tunnelConnection(self): 

195 while True: 

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

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

198 if data == '': 

199 return 

200 # Pass the request to the server 

201 tosend = self.prepareRequest(data) 

202 self.relaySocket.send(tosend) 

203 # Send the response back to the client 

204 self.transferResponse()